From 7d22bbc00f49356cd5fec3565a19376ef8b0ef05 Mon Sep 17 00:00:00 2001
From: Bryan Kok <bryan.wyern1@gmail.com>
Date: Sat, 17 Oct 2020 23:52:18 +0800
Subject: [PATCH 001/389] Trim spurious whitespace of nicknames

---
 src/components/views/settings/ProfileSettings.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 92851ccaa0..294f80acd1 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -77,10 +77,12 @@ export default class ProfileSettings extends React.Component {
         const client = MatrixClientPeg.get();
         const newState = {};
 
+        const displayName = this.state.displayName.trim();
         try {
             if (this.state.originalDisplayName !== this.state.displayName) {
-                await client.setDisplayName(this.state.displayName);
-                newState.originalDisplayName = this.state.displayName;
+                await client.setDisplayName(displayName);
+                newState.originalDisplayName = displayName;
+                newState.displayName = displayName;
             }
 
             if (this.state.avatarFile) {

From fcbaea640daf3a036d55cb1bda5d7fed552c2d4e Mon Sep 17 00:00:00 2001
From: Bryan Kok <bryan.wyern1@gmail.com>
Date: Sun, 18 Oct 2020 14:36:50 +0800
Subject: [PATCH 002/389] Trim room names changed through the UI

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

diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js
index ca09c3093a..b894663c16 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.js
@@ -95,10 +95,11 @@ export default class RoomProfileSettings extends React.Component {
         const newState = {};
 
         // TODO: What do we do about errors?
-
+        const displayName = this.state.displayName.trim();
         if (this.state.originalDisplayName !== this.state.displayName) {
-            await client.setRoomName(this.props.roomId, this.state.displayName);
-            newState.originalDisplayName = this.state.displayName;
+            await client.setRoomName(this.props.roomId, displayName);
+            newState.originalDisplayName = displayName;
+            newState.displayName = displayName;
         }
 
         if (this.state.avatarFile) {

From 2f988bc97fc1e414bd70149ac5f77301a3ef2833 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 26 Nov 2020 13:51:03 +0100
Subject: [PATCH 003/389] Added UI

---
 .../views/settings/SpellCheckSettings.tsx     | 111 ++++++++++++++++++
 .../tabs/user/GeneralUserSettingsTab.js       |  18 +++
 2 files changed, 129 insertions(+)
 create mode 100644 src/components/views/settings/SpellCheckSettings.tsx

diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx
new file mode 100644
index 0000000000..1bdcd882c9
--- /dev/null
+++ b/src/components/views/settings/SpellCheckSettings.tsx
@@ -0,0 +1,111 @@
+/*
+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 LanguageDropdown from "../../../components/views/elements/LanguageDropdown";
+import AccessibleButton from "../../../components/views/elements/AccessibleButton";
+import {_t} from "../../../languageHandler";
+
+interface ExistingSpellCheckLanguageIProps {
+    language: string,
+    onRemoved(language: string),
+};
+
+interface SpellCheckLanguagesIProps {
+    languages: Array<string>,
+    onLanguagesChange(languages: Array<string>),
+};
+
+interface SpellCheckLanguagesIState {
+    newLanguage: string,
+}
+
+export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
+    _onRemove = (e) => {
+        e.stopPropagation();
+        e.preventDefault();
+
+        return this.props.onRemoved(this.props.language);
+    };
+
+    render() {
+        return (
+            <div className="mx_ExistingSpellCheckerLanguage">
+                <span className="mx_ExistingSpellCheckerLanguage_language">{this.props.language}</span>
+                <AccessibleButton onClick={this._onRemove} kind="danger_sm">
+                    {_t("Remove")}
+                </AccessibleButton>
+            </div>
+        );
+    }
+}
+
+export default class SpellCheckLanguages extends React.Component<SpellCheckLanguagesIProps, SpellCheckLanguagesIState> {
+    constructor(props) {
+        super(props);
+        this.state = {
+            newLanguage: "",
+        }
+    }
+
+    _onRemoved = (language) => {
+        const languages = this.props.languages.filter((e) => e !== language);
+        this.props.onLanguagesChange(languages);
+    };
+
+    _onAddClick = (e) => {
+        e.stopPropagation();
+        e.preventDefault();
+
+        const language = this.state.newLanguage;
+        
+        if (!language) return;
+        if (this.props.languages.includes(language)) return;
+
+        this.props.languages.push(language)
+        this.props.onLanguagesChange(this.props.languages);
+    };
+
+    _onNewLanguageChange = (language: string) => {
+        if (this.state.newLanguage === language) return;
+        this.setState({newLanguage: language});
+    };
+
+    render() {
+        const existingSpellCheckLanguages = this.props.languages.map((e) => {
+            return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
+        });
+
+        let addButton = (
+            <AccessibleButton onClick={this._onAddClick} kind="primary">
+                {_t("Add")}
+            </AccessibleButton>
+        );
+
+        return (
+            <div className="mx_SpellCheckerLanguages">
+                {existingSpellCheckLanguages}
+                <form onSubmit={this._onAddClick} noValidate={true}
+                      className="mx_mx_SpellCheckerLanguages_new">
+                    <LanguageDropdown className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
+                                      value={this.state.newLanguage}
+                                      onOptionChange={this._onNewLanguageChange} />
+                    {addButton}
+                </form>
+            </div>
+        );
+    };
+}
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 35285351ab..6d04d83047 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -22,6 +22,7 @@ import ProfileSettings from "../../ProfileSettings";
 import * as languageHandler from "../../../../../languageHandler";
 import SettingsStore from "../../../../../settings/SettingsStore";
 import LanguageDropdown from "../../../elements/LanguageDropdown";
+import SpellCheckSettings from "../../SpellCheckSettings"
 import AccessibleButton from "../../../elements/AccessibleButton";
 import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
 import PropTypes from "prop-types";
@@ -49,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
 
         this.state = {
             language: languageHandler.getCurrentLanguage(),
+            spellCheckLanguages: [],
             haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
             serverSupportsSeparateAddAndBind: null,
             idServerHasUnsignedTerms: false,
@@ -182,6 +184,10 @@ export default class GeneralUserSettingsTab extends React.Component {
         PlatformPeg.get().reload();
     };
 
+    _onSpellCheckLanguagesChange = (languages) => {
+        this.setState({spellCheckLanguages: languages})
+    };
+
     _onPasswordChangeError = (err) => {
         // TODO: Figure out a design that doesn't involve replacing the current dialog
         let errMsg = err.error || "";
@@ -303,6 +309,17 @@ export default class GeneralUserSettingsTab extends React.Component {
         );
     }
 
+    _renderSpellCheckSection() {
+        return (
+             <div className="mx_SettingsTab_section">
+                <span className="mx_SettingsTab_subheading">{_t("Spell checking")}</span>
+                <SpellCheckSettings className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
+                            languages={this.state.spellCheckLanguages}
+                            onLanguagesChange={this._onSpellCheckLanguagesChange} />
+            </div>
+        );
+    }
+
     _renderDiscoverySection() {
         const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
 
@@ -409,6 +426,7 @@ export default class GeneralUserSettingsTab extends React.Component {
                 {this._renderProfileSection()}
                 {this._renderAccountSection()}
                 {this._renderLanguageSection()}
+                {this._renderSpellCheckSection()}
                 { discoverySection }
                 {this._renderIntegrationManagerSection() /* Has its own title */}
                 { accountManagementSection }

From 051368eaab50e4a7d6f2ce554ddff0ed957bb965 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 26 Nov 2020 13:53:22 +0100
Subject: [PATCH 004/389] Fix i18n

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

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 0d50128f32..7fbcc1a350 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1109,6 +1109,7 @@
     "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.",
     "Manage integrations": "Manage integrations",
     "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.",
+    "Add": "Add",
     "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).",
     "Checking for an update...": "Checking for an update...",
     "No update available.": "No update available.",
@@ -1140,6 +1141,7 @@
     "Set a new account password...": "Set a new account password...",
     "Account": "Account",
     "Language and region": "Language and region",
+    "Spell checking": "Spell checking",
     "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
     "Account management": "Account management",
     "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
@@ -1337,7 +1339,6 @@
     "Invalid Email Address": "Invalid Email Address",
     "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address",
     "Unable to add email address": "Unable to add email address",
-    "Add": "Add",
     "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.",
     "Email Address": "Email Address",
     "Remove %(phone)s?": "Remove %(phone)s?",

From 557e650a2c2e2eb24584f21bd4175e69cc7500a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 28 Nov 2020 19:37:49 +0100
Subject: [PATCH 005/389] Added spell-check-languages setting

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

diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 31e133be72..409cd293d2 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -402,6 +402,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
         default: "en",
     },
+    "spell-check-languages": {
+        supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
+        default: [],
+    },
     "breadcrumb_rooms": {
         // not really a setting
         supportedLevels: [SettingLevel.ACCOUNT],

From 43daec03e24820a485f97da0b5cb0311f137e729 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 28 Nov 2020 19:38:52 +0100
Subject: [PATCH 006/389] Added setSpellCheckLanguages() method

---
 src/languageHandler.tsx | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index b61f57d4b3..9b9e304294 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -346,6 +346,13 @@ export function setLanguage(preferredLangs: string | string[]) {
     });
 }
 
+export function setSpellCheckLanguages(preferredLangs: string[]) {
+    const plaf = PlatformPeg.get();
+    if (plaf) {
+        plaf.setLanguage(preferredLangs);
+    }
+}
+
 export function getAllLanguagesFromJson() {
     return getLangsJson().then((langsObject) => {
         const langs = [];

From 5e4f9907cf87e02b41791ce729b38d7474dcbf01 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 28 Nov 2020 19:39:09 +0100
Subject: [PATCH 007/389] Added persistance

---
 .../views/settings/tabs/user/GeneralUserSettingsTab.js        | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 6d04d83047..585f4fd5b7 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -50,7 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
 
         this.state = {
             language: languageHandler.getCurrentLanguage(),
-            spellCheckLanguages: [],
+            spellCheckLanguages: SettingsStore.getValue("spell-check-languages", null, /*excludeDefault=*/true),
             haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
             serverSupportsSeparateAddAndBind: null,
             idServerHasUnsignedTerms: false,
@@ -185,7 +185,9 @@ export default class GeneralUserSettingsTab extends React.Component {
     };
 
     _onSpellCheckLanguagesChange = (languages) => {
+        SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages);
         this.setState({spellCheckLanguages: languages})
+        PlatformPeg.get().reload();
     };
 
     _onPasswordChangeError = (err) => {

From f0bbed0c44270f8411c0ce0f4ee0cf08142a1c1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 29 Nov 2020 12:17:07 +0100
Subject: [PATCH 008/389] Allow default value

---
 .../views/settings/tabs/user/GeneralUserSettingsTab.js          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 585f4fd5b7..68a16463b0 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -50,7 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
 
         this.state = {
             language: languageHandler.getCurrentLanguage(),
-            spellCheckLanguages: SettingsStore.getValue("spell-check-languages", null, /*excludeDefault=*/true),
+            spellCheckLanguages: SettingsStore.getValue("spell-check-languages", null, false),
             haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
             serverSupportsSeparateAddAndBind: null,
             idServerHasUnsignedTerms: false,

From 8f40cd39fda1ee83ec6b177ba7935f913ed5a45b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 29 Nov 2020 12:29:05 +0100
Subject: [PATCH 009/389] Added styling

---
 res/css/_components.scss                      |  1 +
 .../views/settings/_SpellCheckLanguages.scss  | 36 +++++++++++++++++++
 .../views/settings/SpellCheckSettings.tsx     |  9 +++--
 .../tabs/user/GeneralUserSettingsTab.js       |  7 ++--
 4 files changed, 44 insertions(+), 9 deletions(-)
 create mode 100644 res/css/views/settings/_SpellCheckLanguages.scss

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 445ed70ff4..1eb4b91a31 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -207,6 +207,7 @@
 @import "./views/settings/_DevicesPanel.scss";
 @import "./views/settings/_E2eAdvancedPanel.scss";
 @import "./views/settings/_EmailAddresses.scss";
+@import "./views/settings/_SpellCheckLanguages.scss";
 @import "./views/settings/_IntegrationManager.scss";
 @import "./views/settings/_Notifications.scss";
 @import "./views/settings/_PhoneNumbers.scss";
diff --git a/res/css/views/settings/_SpellCheckLanguages.scss b/res/css/views/settings/_SpellCheckLanguages.scss
new file mode 100644
index 0000000000..734f669f0e
--- /dev/null
+++ b/res/css/views/settings/_SpellCheckLanguages.scss
@@ -0,0 +1,36 @@
+/*
+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.
+*/
+
+.mx_ExistingSpellCheckLanguage {
+    display: flex;
+    align-items: center;
+    margin-bottom: 5px;
+}
+
+.mx_ExistingSpellCheckLanguage_language {
+    flex: 1;
+    margin-right: 10px;
+}
+
+.mx_GeneralUserSettingsTab_spellCheckLanguageInput {
+    margin-top: 1em;
+    margin-bottom: 1em;
+}
+
+.mx_SpellCheckLanguages {
+    @mixin mx_Settings_fullWidthField;
+}
\ No newline at end of file
diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx
index 1bdcd882c9..befd98112e 100644
--- a/src/components/views/settings/SpellCheckSettings.tsx
+++ b/src/components/views/settings/SpellCheckSettings.tsx
@@ -43,8 +43,8 @@ export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellChe
 
     render() {
         return (
-            <div className="mx_ExistingSpellCheckerLanguage">
-                <span className="mx_ExistingSpellCheckerLanguage_language">{this.props.language}</span>
+            <div className="mx_ExistingSpellCheckLanguage">
+                <span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
                 <AccessibleButton onClick={this._onRemove} kind="danger_sm">
                     {_t("Remove")}
                 </AccessibleButton>
@@ -96,10 +96,9 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
         );
 
         return (
-            <div className="mx_SpellCheckerLanguages">
+            <div className="mx_SpellCheckLanguages">
                 {existingSpellCheckLanguages}
-                <form onSubmit={this._onAddClick} noValidate={true}
-                      className="mx_mx_SpellCheckerLanguages_new">
+                <form onSubmit={this._onAddClick} noValidate={true}>
                     <LanguageDropdown className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
                                       value={this.state.newLanguage}
                                       onOptionChange={this._onNewLanguageChange} />
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 68a16463b0..258ff6d318 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -313,11 +313,10 @@ export default class GeneralUserSettingsTab extends React.Component {
 
     _renderSpellCheckSection() {
         return (
-             <div className="mx_SettingsTab_section">
+            <div className="mx_SettingsTab_section">
                 <span className="mx_SettingsTab_subheading">{_t("Spell checking")}</span>
-                <SpellCheckSettings className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
-                            languages={this.state.spellCheckLanguages}
-                            onLanguagesChange={this._onSpellCheckLanguagesChange} />
+                <SpellCheckSettings languages={this.state.spellCheckLanguages}
+                                    onLanguagesChange={this._onSpellCheckLanguagesChange} />
             </div>
         );
     }

From 7609f2004e6a004c22e0da189f58cee823bf4468 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 29 Nov 2020 13:22:50 +0100
Subject: [PATCH 010/389] Added newline to end

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

diff --git a/res/css/views/settings/_SpellCheckLanguages.scss b/res/css/views/settings/_SpellCheckLanguages.scss
index 734f669f0e..ddfa0bf9e0 100644
--- a/res/css/views/settings/_SpellCheckLanguages.scss
+++ b/res/css/views/settings/_SpellCheckLanguages.scss
@@ -33,4 +33,4 @@ limitations under the License.
 
 .mx_SpellCheckLanguages {
     @mixin mx_Settings_fullWidthField;
-}
\ No newline at end of file
+}

From ead00dcdede9e6a24904599baba39bc91de0681a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 29 Nov 2020 14:46:09 +0100
Subject: [PATCH 011/389] Set spell-check languages without reloading

---
 .../views/settings/tabs/user/GeneralUserSettingsTab.js      | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 258ff6d318..ad7e04d677 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -187,7 +187,11 @@ export default class GeneralUserSettingsTab extends React.Component {
     _onSpellCheckLanguagesChange = (languages) => {
         SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages);
         this.setState({spellCheckLanguages: languages})
-        PlatformPeg.get().reload();
+        
+        const plaf = PlatformPeg.get();
+        if (plaf) {
+            plaf.setLanguage(languages);
+        }
     };
 
     _onPasswordChangeError = (err) => {

From 38080c5b2bccafb7dae5c9737e69ad7f295f1d7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 29 Nov 2020 20:46:47 +0100
Subject: [PATCH 012/389] Added getAvailableSpellCheckLanguages() methods

---
 src/BasePlatform.ts     | 4 ++++
 src/languageHandler.tsx | 5 +++++
 2 files changed, 9 insertions(+)

diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index 0a1f06f0b3..9ac35092a7 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -237,6 +237,10 @@ export default abstract class BasePlatform {
 
     setLanguage(preferredLangs: string[]) {}
 
+    getAvailableSpellCheckLanguages(): Promise<string[]> | null {
+        return null;
+    }
+
     protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
         const url = new URL(window.location.href);
         url.hash = fragmentAfterLogin || "";
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index 9b9e304294..b827e83ded 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -353,6 +353,11 @@ export function setSpellCheckLanguages(preferredLangs: string[]) {
     }
 }
 
+export async function getAvailableSpellCheckLanguages(): Promise<string[]> {
+    const plaf = PlatformPeg.get();
+    return plaf.getAvailableSpellCheckLanguages();
+}
+
 export function getAllLanguagesFromJson() {
     return getLangsJson().then((langsObject) => {
         const langs = [];

From 5d9f5ba979d3fc89bf6eec5ad2f319bea7168aec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Mon, 30 Nov 2020 08:35:51 +0100
Subject: [PATCH 013/389] Fix indentation

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

diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js
index e37109caff..03ec456af5 100644
--- a/src/components/views/elements/LanguageDropdown.js
+++ b/src/components/views/elements/LanguageDropdown.js
@@ -100,10 +100,10 @@ export default class LanguageDropdown extends React.Component {
         let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
         let value = null;
         if (language) {
-          value = this.props.value || language;
+            value = this.props.value || language;
         } else {
-          language = navigator.language || navigator.userLanguage;
-          value = this.props.value || language;
+            language = navigator.language || navigator.userLanguage;
+            value = this.props.value || language;
         }
 
         return <Dropdown

From b207a877ebb4aca95a4eb42695b2821250ffb385 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 1 Dec 2020 16:59:02 +0100
Subject: [PATCH 014/389] Change label

---
 .../views/settings/tabs/user/GeneralUserSettingsTab.js          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index ad7e04d677..8d06ea3b36 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -318,7 +318,7 @@ export default class GeneralUserSettingsTab extends React.Component {
     _renderSpellCheckSection() {
         return (
             <div className="mx_SettingsTab_section">
-                <span className="mx_SettingsTab_subheading">{_t("Spell checking")}</span>
+                <span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
                 <SpellCheckSettings languages={this.state.spellCheckLanguages}
                                     onLanguagesChange={this._onSpellCheckLanguagesChange} />
             </div>

From cf61d50df40614a45c36c0d9886c3583ca69513e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 1 Dec 2020 16:59:21 +0100
Subject: [PATCH 015/389] Added SpellCheckLanguagesDropdown

---
 .../elements/SpellCheckLanguagesDropdown.tsx  | 125 ++++++++++++++++++
 .../views/settings/SpellCheckSettings.tsx     |   4 +-
 2 files changed, 127 insertions(+), 2 deletions(-)
 create mode 100644 src/components/views/elements/SpellCheckLanguagesDropdown.tsx

diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
new file mode 100644
index 0000000000..db158fa3dd
--- /dev/null
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -0,0 +1,125 @@
+/*
+Copyright 2017 Marcel Radzio (MTRNord)
+Copyright 2017 Vector Creations Ltd.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Dropdown from "../../views/elements/Dropdown"
+import PlatformPeg from "../../../PlatformPeg";
+import * as sdk from '../../../index';
+import * as languageHandler from '../../../languageHandler';
+import SettingsStore from "../../../settings/SettingsStore";
+import { _t } from "../../../languageHandler";
+
+function languageMatchesSearchQuery(query, language) {
+    if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
+    if (language.value.toUpperCase() === query.toUpperCase()) return true;
+    return false;
+}
+
+interface SpellCheckLanguagesDropdownIProps {
+    className: string,
+    value: string,
+    onOptionChange(language: string),
+};
+
+interface SpellCheckLanguagesDropdownIState {
+    searchQuery: string,
+    languages: any,
+}
+
+export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps, SpellCheckLanguagesDropdownIState> {
+    constructor(props) {
+        super(props);
+        this._onSearchChange = this._onSearchChange.bind(this);
+
+        this.state = {
+            searchQuery: '',
+            languages: null,
+        };
+    }
+
+    componentDidMount() {
+        languageHandler.getAvailableSpellCheckLanguages().then((languages) => {
+            languages.sort(function(a, b) {
+                if (a < b) return -1;
+                if (a > b) return 1;
+                return 0;
+            });
+            var langs = [];
+            languages.forEach((language) => {
+                langs.push({
+                    label: language,
+                    value: language,
+                })
+            })
+            this.setState({languages: langs});
+        }).catch((e) => {
+            this.setState({languages: ['en']});
+        });
+    }
+
+    _onSearchChange(search) {
+        this.setState({
+            searchQuery: search,
+        });
+    }
+
+    render() {
+        if (this.state.languages === null) {
+            const Spinner = sdk.getComponent('elements.Spinner');
+            return <Spinner />;
+        }
+
+        let displayedLanguages;
+        if (this.state.searchQuery) {
+            displayedLanguages = this.state.languages.filter((lang) => {
+                return languageMatchesSearchQuery(this.state.searchQuery, lang);
+            });
+        } else {
+            displayedLanguages = this.state.languages;
+        }
+
+        const options = displayedLanguages.map((language) => {
+            return <div key={language.value}>
+                { language.label }
+            </div>;
+        });
+
+        // default value here too, otherwise we need to handle null / undefined;
+        // values between mounting and the initial value propgating
+        let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
+        let value = null;
+        if (language) {
+            value = this.props.value || language;
+        } else {
+            language = navigator.language || navigator.userLanguage;
+            value = this.props.value || language;
+        }
+
+        return <Dropdown
+            id="mx_LanguageDropdown"
+            className={this.props.className}
+            onOptionChange={this.props.onOptionChange}
+            onSearchChange={this._onSearchChange}
+            searchEnabled={true}
+            value={value}
+            label={_t("Language Dropdown")}>
+            { options }
+        </Dropdown>;
+    }
+}
diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx
index befd98112e..37476d5f34 100644
--- a/src/components/views/settings/SpellCheckSettings.tsx
+++ b/src/components/views/settings/SpellCheckSettings.tsx
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import React from 'react';
-import LanguageDropdown from "../../../components/views/elements/LanguageDropdown";
+import SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
 import AccessibleButton from "../../../components/views/elements/AccessibleButton";
 import {_t} from "../../../languageHandler";
 
@@ -99,7 +99,7 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
             <div className="mx_SpellCheckLanguages">
                 {existingSpellCheckLanguages}
                 <form onSubmit={this._onAddClick} noValidate={true}>
-                    <LanguageDropdown className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
+                    <SpellCheckLanguagesDropdown className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
                                       value={this.state.newLanguage}
                                       onOptionChange={this._onNewLanguageChange} />
                     {addButton}

From a6d6af1a937fb6bc6cec2e320fbff453bef3c680 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 1 Dec 2020 17:19:45 +0100
Subject: [PATCH 016/389] Added defaults

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

diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 409cd293d2..c83dbab897 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -404,7 +404,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
     },
     "spell-check-languages": {
         supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
-        default: [],
+        default: ["en"],
     },
     "breadcrumb_rooms": {
         // not really a setting

From e9203d75715dbd6a677849dbb83a3d4706b2e6e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 1 Dec 2020 17:21:23 +0100
Subject: [PATCH 017/389] Removed unnecessary imports

---
 src/components/views/elements/SpellCheckLanguagesDropdown.tsx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
index db158fa3dd..5e0fe3132c 100644
--- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -16,10 +16,8 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 
 import Dropdown from "../../views/elements/Dropdown"
-import PlatformPeg from "../../../PlatformPeg";
 import * as sdk from '../../../index';
 import * as languageHandler from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";

From 8287f197f40869941d402e45da87c88d19514545 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 1 Dec 2020 19:49:31 +0100
Subject: [PATCH 018/389] Fix 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 282c1ce686..9ccd0e1e75 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1149,7 +1149,7 @@
     "Set a new account password...": "Set a new account password...",
     "Account": "Account",
     "Language and region": "Language and region",
-    "Spell checking": "Spell checking",
+    "Spell check dictionaries": "Spell check dictionaries",
     "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
     "Account management": "Account management",
     "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",

From 44a363f188fb95927fff942b4c6b5a3914dbe31d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 1 Dec 2020 20:16:48 +0100
Subject: [PATCH 019/389] Fix default value

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

diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index c83dbab897..3540767a99 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -404,7 +404,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
     },
     "spell-check-languages": {
         supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
-        default: ["en"],
+        default: ["en-US"],
     },
     "breadcrumb_rooms": {
         // not really a setting

From 3c2bb6e4f6d19e337d902613adbddf42fcba2f3b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 1 Dec 2020 20:17:24 +0100
Subject: [PATCH 020/389] Cleanup

---
 src/BasePlatform.ts                                          | 2 ++
 .../views/settings/tabs/user/GeneralUserSettingsTab.js       | 5 +----
 src/languageHandler.tsx                                      | 2 +-
 3 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index 9ac35092a7..2af2ea51c5 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -237,6 +237,8 @@ export default abstract class BasePlatform {
 
     setLanguage(preferredLangs: string[]) {}
 
+    setSpellCheckLanguages(preferredLangs: string[]) {}
+
     getAvailableSpellCheckLanguages(): Promise<string[]> | null {
         return null;
     }
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 8d06ea3b36..6ed887d749 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -188,10 +188,7 @@ export default class GeneralUserSettingsTab extends React.Component {
         SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages);
         this.setState({spellCheckLanguages: languages})
         
-        const plaf = PlatformPeg.get();
-        if (plaf) {
-            plaf.setLanguage(languages);
-        }
+        languageHandler.setSpellCheckLanguages(languages);
     };
 
     _onPasswordChangeError = (err) => {
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index b827e83ded..38d3c8347a 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -349,7 +349,7 @@ export function setLanguage(preferredLangs: string | string[]) {
 export function setSpellCheckLanguages(preferredLangs: string[]) {
     const plaf = PlatformPeg.get();
     if (plaf) {
-        plaf.setLanguage(preferredLangs);
+        plaf.setSpellCheckLanguages(preferredLangs);
     }
 }
 

From db5bc0cb7ade92fc056283af639e3c782d384e4f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 1 Dec 2020 20:36:25 +0100
Subject: [PATCH 021/389] Fix formatting

---
 .../elements/SpellCheckLanguagesDropdown.tsx    |  7 ++++---
 .../views/settings/SpellCheckSettings.tsx       | 17 +++++++++--------
 .../tabs/user/GeneralUserSettingsTab.js         |  6 +++---
 3 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
index 5e0fe3132c..53c3f310b7 100644
--- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -33,14 +33,15 @@ interface SpellCheckLanguagesDropdownIProps {
     className: string,
     value: string,
     onOptionChange(language: string),
-};
+}
 
 interface SpellCheckLanguagesDropdownIState {
     searchQuery: string,
     languages: any,
 }
 
-export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps, SpellCheckLanguagesDropdownIState> {
+export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps,
+                                                                         SpellCheckLanguagesDropdownIState> {
     constructor(props) {
         super(props);
         this._onSearchChange = this._onSearchChange.bind(this);
@@ -58,7 +59,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
                 if (a > b) return 1;
                 return 0;
             });
-            var langs = [];
+            const langs = [];
             languages.forEach((language) => {
                 langs.push({
                     label: language,
diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx
index 37476d5f34..bfe0774570 100644
--- a/src/components/views/settings/SpellCheckSettings.tsx
+++ b/src/components/views/settings/SpellCheckSettings.tsx
@@ -22,12 +22,12 @@ import {_t} from "../../../languageHandler";
 interface ExistingSpellCheckLanguageIProps {
     language: string,
     onRemoved(language: string),
-};
+}
 
 interface SpellCheckLanguagesIProps {
     languages: Array<string>,
     onLanguagesChange(languages: Array<string>),
-};
+}
 
 interface SpellCheckLanguagesIState {
     newLanguage: string,
@@ -71,7 +71,7 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
         e.preventDefault();
 
         const language = this.state.newLanguage;
-        
+
         if (!language) return;
         if (this.props.languages.includes(language)) return;
 
@@ -89,7 +89,7 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
             return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
         });
 
-        let addButton = (
+        const addButton = (
             <AccessibleButton onClick={this._onAddClick} kind="primary">
                 {_t("Add")}
             </AccessibleButton>
@@ -99,12 +99,13 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
             <div className="mx_SpellCheckLanguages">
                 {existingSpellCheckLanguages}
                 <form onSubmit={this._onAddClick} noValidate={true}>
-                    <SpellCheckLanguagesDropdown className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
-                                      value={this.state.newLanguage}
-                                      onOptionChange={this._onNewLanguageChange} />
+                    <SpellCheckLanguagesDropdown
+                        className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
+                        value={this.state.newLanguage}
+                        onOptionChange={this._onNewLanguageChange} />
                     {addButton}
                 </form>
             </div>
         );
-    };
+    }
 }
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 6ed887d749..95a8abbb24 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -22,7 +22,7 @@ import ProfileSettings from "../../ProfileSettings";
 import * as languageHandler from "../../../../../languageHandler";
 import SettingsStore from "../../../../../settings/SettingsStore";
 import LanguageDropdown from "../../../elements/LanguageDropdown";
-import SpellCheckSettings from "../../SpellCheckSettings"
+import SpellCheckSettings from "../../SpellCheckSettings";
 import AccessibleButton from "../../../elements/AccessibleButton";
 import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
 import PropTypes from "prop-types";
@@ -186,8 +186,8 @@ export default class GeneralUserSettingsTab extends React.Component {
 
     _onSpellCheckLanguagesChange = (languages) => {
         SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages);
-        this.setState({spellCheckLanguages: languages})
-        
+        this.setState({spellCheckLanguages: languages});
+
         languageHandler.setSpellCheckLanguages(languages);
     };
 

From bab541a652e402c1aede8caa00b22b13a2adb0c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 2 Dec 2020 20:14:58 +0100
Subject: [PATCH 022/389] Hide spell-check settings if not using Electron

---
 src/BasePlatform.ts                                       | 8 ++++++++
 .../views/settings/tabs/user/GeneralUserSettingsTab.js    | 5 ++++-
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index 2af2ea51c5..54d15675cb 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -128,6 +128,14 @@ export default abstract class BasePlatform {
         hideUpdateToast();
     }
 
+    /**
+     * Return true if platform supports multi-language
+     * spell-checking, otherwise false.
+     */
+    supportsMultiLanguageSpellCheck(): boolean {
+        return false;
+    }
+
     /**
      * Returns true if the platform supports displaying
      * notifications, otherwise false.
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 95a8abbb24..4d1210dc40 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -400,6 +400,9 @@ export default class GeneralUserSettingsTab extends React.Component {
     }
 
     render() {
+        const plaf = PlatformPeg.get();
+        const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck() ? true : false;
+
         const discoWarning = this.state.requiredPolicyInfo.hasTerms
             ? <img className='mx_GeneralUserSettingsTab_warningIcon'
                 src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
@@ -428,7 +431,7 @@ export default class GeneralUserSettingsTab extends React.Component {
                 {this._renderProfileSection()}
                 {this._renderAccountSection()}
                 {this._renderLanguageSection()}
-                {this._renderSpellCheckSection()}
+                {supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null}
                 { discoverySection }
                 {this._renderIntegrationManagerSection() /* Has its own title */}
                 { accountManagementSection }

From fa19adcfe0412afc7ffdc67722f0ad2858e61edb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 3 Dec 2020 11:50:08 +0100
Subject: [PATCH 023/389] Simplifie

---
 .../views/settings/tabs/user/GeneralUserSettingsTab.js          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 4d1210dc40..febbcc8e36 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -401,7 +401,7 @@ export default class GeneralUserSettingsTab extends React.Component {
 
     render() {
         const plaf = PlatformPeg.get();
-        const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck() ? true : false;
+        const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
 
         const discoWarning = this.state.requiredPolicyInfo.hasTerms
             ? <img className='mx_GeneralUserSettingsTab_warningIcon'

From b8008b5f190020e3592be190bd0fae5876aa29dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 3 Dec 2020 11:50:20 +0100
Subject: [PATCH 024/389] Added in if statement

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

diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index 38d3c8347a..985719fce7 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -355,7 +355,9 @@ export function setSpellCheckLanguages(preferredLangs: string[]) {
 
 export async function getAvailableSpellCheckLanguages(): Promise<string[]> {
     const plaf = PlatformPeg.get();
-    return plaf.getAvailableSpellCheckLanguages();
+    if (plaf) {
+        return plaf.getAvailableSpellCheckLanguages();
+    }
 }
 
 export function getAllLanguagesFromJson() {

From 89bc4435945bfb207355cf5e5e290925f7d7f7aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 16 Dec 2020 16:02:27 +0100
Subject: [PATCH 025/389] Fix file drop UI
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_MainSplit.scss |  2 +-
 res/css/structures/_RoomView.scss  | 13 +++++--------
 2 files changed, 6 insertions(+), 9 deletions(-)

diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss
index ad1656efbb..3de68b000d 100644
--- a/res/css/structures/_MainSplit.scss
+++ b/res/css/structures/_MainSplit.scss
@@ -22,7 +22,7 @@ limitations under the License.
 }
 
 .mx_MainSplit > .mx_RightPanel_ResizeWrapper {
-    padding: 5px;
+    padding: 0 5px 5px 5px;
     // margin left to not allow the handle to not encroach on the space for the scrollbar
     margin-left: 8px;
     height: calc(100vh - 51px); // height of .mx_RoomHeader.light-panel
diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 572c7166d2..0a70b027ae 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -23,24 +23,21 @@ limitations under the License.
 .mx_RoomView_fileDropTarget {
     min-width: 0px;
     width: 100%;
+    height: 100%;
+
+    margin-left: 6.25px;
+
     font-size: $font-18px;
     text-align: center;
 
     pointer-events: none;
 
-    padding-left: 12px;
-    padding-right: 12px;
-    margin-left: -12px;
-
     border-top-left-radius: 10px;
     border-top-right-radius: 10px;
 
     background-color: $droptarget-bg-color;
-    border: 2px #e1dddd solid;
-    border-bottom: none;
+
     position: absolute;
-    top: 52px;
-    bottom: 0px;
     z-index: 3000;
 }
 

From 41e2ffdf0df43104ef171b690b344a6e22b286f1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 16 Dec 2020 19:51:49 +0100
Subject: [PATCH 026/389] Added background
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 0a70b027ae..9292a400bc 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -39,13 +39,19 @@ limitations under the License.
 
     position: absolute;
     z-index: 3000;
+
+    display: flex;
+    justify-content: center;
+    align-items: center;
 }
 
 .mx_RoomView_fileDropTargetLabel {
-    top: 50%;
-    width: 100%;
-    margin-top: -50px;
     position: absolute;
+
+    border-radius: 10px;
+    padding: 10px;
+
+    background-color: $menu-bg-color;
 }
 
 .mx_RoomView_auxPanel {

From da97d18332c5740499913506b0e059e5b4c7616c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 16 Dec 2020 21:33:05 +0100
Subject: [PATCH 027/389] Added a comment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 9292a400bc..dd63be3a11 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -25,6 +25,7 @@ limitations under the License.
     width: 100%;
     height: 100%;
 
+    // This is an ugly fix for centering this element
     margin-left: 6.25px;
 
     font-size: $font-18px;

From dcb30b72b0ed1adc6fb075ee9cc26ca0338177bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 17 Dec 2020 13:25:22 +0100
Subject: [PATCH 028/389] Fix left panel resizer
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_MainSplit.scss     | 32 ++++++++++++++++----------
 res/css/structures/_RoomView.scss      | 12 ++++++----
 src/components/structures/RoomView.tsx | 28 +++++++++++-----------
 3 files changed, 43 insertions(+), 29 deletions(-)

diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss
index 3de68b000d..6875ef12e0 100644
--- a/res/css/structures/_MainSplit.scss
+++ b/res/css/structures/_MainSplit.scss
@@ -22,22 +22,30 @@ limitations under the License.
 }
 
 .mx_MainSplit > .mx_RightPanel_ResizeWrapper {
-    padding: 0 5px 5px 5px;
-    // margin left to not allow the handle to not encroach on the space for the scrollbar
-    margin-left: 8px;
+    padding: 0 5px 5px 0px;
     height: calc(100vh - 51px); // height of .mx_RoomHeader.light-panel
+    
+    .mx_RightPanel_ResizeHandle {
+        width: 9px;
+    }
 
     &:hover .mx_RightPanel_ResizeHandle {
-        // Need to use important to override element style attributes
-        // set by re-resizable
-        top: 50% !important;
-        transform: translate(0, -50%);
+        &::before {
+            position: absolute;
+            left: 6px;
+            top: 50%;
+            transform: translate(0, -50%);
 
-        height: 64px !important; // to match width of the ones on roomlist
-        width: 4px !important;
-        border-radius: 4px !important;
+            height: 64px;
+            width: 4px;
+            border-radius: 4px;
 
-        background-color: $primary-fg-color;
-        opacity: 0.8;
+            content: ' ';
+
+            background-color: $primary-fg-color;
+            opacity: 0.8;
+
+            margin-left: -10px;
+        }
     }
 }
diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index dd63be3a11..0a12a86c33 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -25,9 +25,6 @@ limitations under the License.
     width: 100%;
     height: 100%;
 
-    // This is an ugly fix for centering this element
-    margin-left: 6.25px;
-
     font-size: $font-18px;
     text-align: center;
 
@@ -120,16 +117,23 @@ limitations under the License.
     height: 50px;
 }
 
-.mx_RoomView_body {
+.mx_RoomView_container {
     position: relative; //for .mx_RoomView_auxPanel_fullHeight
     display: flex;
     flex-direction: column;
+}
+
+.mx_RoomView_body {
+    display: flex;
+    flex-direction: column;
     flex: 1;
     min-width: 0;
 
     .mx_RoomView_messagePanel, .mx_RoomView_messagePanelSpinner, .mx_RoomView_messagePanelSearchSpinner {
         order: 2;
     }
+
+    margin-right: 10px;
 }
 
 .mx_RoomView_body .mx_RoomView_timeline {
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 0ee847fbc9..3d62c06e4b 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -2003,22 +2003,24 @@ export default class RoomView extends React.Component<IProps, IState> {
                             appsShown={this.state.showApps}
                         />
                         <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
-                            <div className="mx_RoomView_body">
+                            <div className={"mx_RoomView_container"}>
                                 {auxPanel}
-                                <div className={timelineClasses}>
-                                    {topUnreadMessagesBar}
-                                    {jumpToBottom}
-                                    {messagePanel}
-                                    {searchResultsPanel}
-                                </div>
-                                <div className={statusBarAreaClass}>
-                                    <div className="mx_RoomView_statusAreaBox">
-                                        <div className="mx_RoomView_statusAreaBox_line" />
-                                        {statusBar}
+                                <div className="mx_RoomView_body">
+                                    <div className={timelineClasses}>
+                                        {topUnreadMessagesBar}
+                                        {jumpToBottom}
+                                        {messagePanel}
+                                        {searchResultsPanel}
                                     </div>
+                                    <div className={statusBarAreaClass}>
+                                        <div className="mx_RoomView_statusAreaBox">
+                                            <div className="mx_RoomView_statusAreaBox_line" />
+                                            {statusBar}
+                                        </div>
+                                    </div>
+                                    {previewBar}
+                                    {messageComposer}
                                 </div>
-                                {previewBar}
-                                {messageComposer}
                             </div>
                         </MainSplit>
                     </ErrorBoundary>

From e70dee08d0ea7b303a51fb807929376b2dad79dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 17 Dec 2020 19:50:59 +0100
Subject: [PATCH 029/389] Fix flickering
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/structures/RoomView.tsx | 42 ++++++++++++++++++++------
 1 file changed, 33 insertions(+), 9 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 3d62c06e4b..67f9663597 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -187,6 +187,7 @@ export interface IState {
     rejecting?: boolean;
     rejectError?: Error;
     hasPinnedWidgets?: boolean;
+    dragCounter: number;
 }
 
 export default class RoomView extends React.Component<IProps, IState> {
@@ -237,6 +238,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             canReply: false,
             useIRCLayout: SettingsStore.getValue("useIRCLayout"),
             matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
+            dragCounter: 0,
         };
 
         this.dispatcherRef = dis.register(this.onAction);
@@ -525,8 +527,8 @@ export default class RoomView extends React.Component<IProps, IState> {
             if (!roomView.ondrop) {
                 roomView.addEventListener('drop', this.onDrop);
                 roomView.addEventListener('dragover', this.onDragOver);
-                roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
-                roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
+                roomView.addEventListener('dragenter', this.onDragEnter);
+                roomView.addEventListener('dragleave', this.onDragLeave);
             }
         }
 
@@ -1108,6 +1110,31 @@ export default class RoomView extends React.Component<IProps, IState> {
         this.updateTopUnreadMessagesBar();
     };
 
+    private onDragEnter = ev => {
+        ev.stopPropagation();
+        ev.preventDefault();
+
+        this.setState({
+            dragCounter: this.state.dragCounter + 1,
+            draggingFile: true,
+        });
+    };
+
+    private onDragLeave = ev => {
+        ev.stopPropagation();
+        ev.preventDefault();
+
+        this.setState({
+            dragCounter: this.state.dragCounter - 1,
+        });
+
+        if (this.state.dragCounter == 0) {
+            this.setState({
+                draggingFile: false,
+            });
+        }
+    };
+
     private onDragOver = ev => {
         ev.stopPropagation();
         ev.preventDefault();
@@ -1115,7 +1142,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         ev.dataTransfer.dropEffect = 'none';
 
         if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
-            this.setState({ draggingFile: true });
             ev.dataTransfer.dropEffect = 'copy';
         }
     };
@@ -1126,14 +1152,12 @@ export default class RoomView extends React.Component<IProps, IState> {
         ContentMessages.sharedInstance().sendContentListToRoom(
             ev.dataTransfer.files, this.state.room.roomId, this.context,
         );
-        this.setState({ draggingFile: false });
         dis.fire(Action.FocusComposer);
-    };
 
-    private onDragLeaveOrEnd = ev => {
-        ev.stopPropagation();
-        ev.preventDefault();
-        this.setState({ draggingFile: false });
+        this.setState({
+            draggingFile: false,
+            dragCounter: this.state.dragCounter - 1,
+        });
     };
 
     private injectSticker(url, info, text) {

From 044e02b06ad46b417d3aa8fc33f24c1374fdcb56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 17 Dec 2020 20:16:53 +0100
Subject: [PATCH 030/389] Remove spaces in empty line
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_MainSplit.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss
index 6875ef12e0..f05f24d0d7 100644
--- a/res/css/structures/_MainSplit.scss
+++ b/res/css/structures/_MainSplit.scss
@@ -24,7 +24,7 @@ limitations under the License.
 .mx_MainSplit > .mx_RightPanel_ResizeWrapper {
     padding: 0 5px 5px 0px;
     height: calc(100vh - 51px); // height of .mx_RoomHeader.light-panel
-    
+
     .mx_RightPanel_ResizeHandle {
         width: 9px;
     }

From 365d252d3f0eb64755f502318c95f855a4404f56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 17 Dec 2020 20:25:12 +0100
Subject: [PATCH 031/389] Fix removing event listeners
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/structures/RoomView.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 67f9663597..d910940a73 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -572,8 +572,8 @@ export default class RoomView extends React.Component<IProps, IState> {
             const roomView = this.roomView.current;
             roomView.removeEventListener('drop', this.onDrop);
             roomView.removeEventListener('dragover', this.onDragOver);
-            roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
-            roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
+            roomView.removeEventListener('dragenter', this.onDragEnter);
+            roomView.removeEventListener('dragleave', this.onDragLeave);
         }
         dis.unregister(this.dispatcherRef);
         if (this.context) {

From 5d7e45e6cf85e14f4143923f7e29642f97965fc6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 17 Dec 2020 20:29:33 +0100
Subject: [PATCH 032/389] Added dragCounter
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/contexts/RoomContext.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts
index 082dcc4e6b..1b9097e337 100644
--- a/src/contexts/RoomContext.ts
+++ b/src/contexts/RoomContext.ts
@@ -42,6 +42,7 @@ const RoomContext = createContext<IState>({
     canReply: false,
     useIRCLayout: false,
     matrixClientIsReady: false,
+    dragCounter: 0,
 });
 RoomContext.displayName = "RoomContext";
 export default RoomContext;

From d589c6100069c5ddae0c7372760c63d40ecd84ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 9 Jan 2021 09:09:14 +0100
Subject: [PATCH 033/389] Added send message button
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_MessageComposer.scss     |  5 +++++
 src/components/views/rooms/MessageComposer.js | 14 ++++++++++++++
 2 files changed, 19 insertions(+)

diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index 71c0db947e..897167f745 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -239,6 +239,7 @@ limitations under the License.
     mask-image: url('$(res)/img/element-icons/call/video-call.svg');
 }
 
+
 .mx_MessageComposer_emoji::before {
     mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
 }
@@ -247,6 +248,10 @@ limitations under the License.
     mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
 }
 
+.mx_MessageComposer_sendMessage::before {
+    mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+}
+
 .mx_MessageComposer_formatting {
     cursor: pointer;
     margin: 0 11px;
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 4ddff8f4b0..86ad3ddbdd 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -393,6 +393,10 @@ export default class MessageComposer extends React.Component {
         });
     }
 
+    sendMessage = () => {
+        this.messageComposerInput._sendMessage();
+    }
+
     render() {
         const controls = [
             this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
@@ -450,6 +454,16 @@ export default class MessageComposer extends React.Component {
                     );
                 }
             }
+
+            if (true) {
+                controls.push((
+                    <AccessibleTooltipButton
+                        className="mx_MessageComposer_button mx_MessageComposer_sendMessage"
+                        onClick={this.sendMessage}
+                        title={_t('Send message')}
+                    />
+                ));
+            }
         } else if (this.state.tombstone) {
             const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
 

From c64b2a585f3c2d1e75392657995d6b1813250f32 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 9 Jan 2021 09:17:40 +0100
Subject: [PATCH 034/389] Added option to disable send button
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js                | 2 +-
 .../views/settings/tabs/user/PreferencesUserSettingsTab.js   | 1 +
 src/settings/Settings.ts                                     | 5 +++++
 3 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 86ad3ddbdd..315b1b78c7 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -455,7 +455,7 @@ export default class MessageComposer extends React.Component {
                 }
             }
 
-            if (true) {
+            if (SettingsStore.getValue("MessageComposerInput.sendButton")) {
                 controls.push((
                     <AccessibleTooltipButton
                         className="mx_MessageComposer_button mx_MessageComposer_sendMessage"
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index 4d8493401e..31971b7167 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -34,6 +34,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'MessageComposerInput.suggestEmoji',
         'sendTypingNotifications',
         'MessageComposerInput.ctrlEnterToSend',
+        `MessageComposerInput.sendButton`,
     ];
 
     static TIMELINE_SETTINGS = [
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index b239b809fe..50d0f919e6 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -336,6 +336,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
         default: false,
     },
+    "MessageComposerInput.sendButton": {
+        supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+        displayName: _td("Show send message button"),
+        default: false,
+    },
     "MessageComposerInput.autoReplaceEmoji": {
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
         displayName: _td('Automatically replace plain text Emoji'),

From 5a42c3f33561fc4f2d9d3ab72c5734a5255d68d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 9 Jan 2021 09:18:10 +0100
Subject: [PATCH 035/389] i18n
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/i18n/strings/en_EN.json | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index ac18ccb23e..6baccf95de 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -799,6 +799,7 @@
     "Show typing notifications": "Show typing notifications",
     "Use Command + Enter to send a message": "Use Command + Enter to send a message",
     "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
+    "Show send message button": "Show send message button",
     "Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
     "Mirror local video feed": "Mirror local video feed",
     "Enable Community Filter Panel": "Enable Community Filter Panel",
@@ -1412,6 +1413,7 @@
     "Send a reply…": "Send a reply…",
     "Send an encrypted message…": "Send an encrypted message…",
     "Send a message…": "Send a message…",
+    "Send message": "Send message",
     "The conversation continues here.": "The conversation continues here.",
     "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
     "You do not have permission to post to this room": "You do not have permission to post to this room",

From 4538274e74938294f840abcae070ac75d052d311 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 9 Jan 2021 09:25:29 +0100
Subject: [PATCH 036/389] Added send message icon
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_MessageComposer.scss |  2 +-
 res/img/element-icons/send-message.svg    | 54 +++++++++++++++++++++++
 2 files changed, 55 insertions(+), 1 deletion(-)
 create mode 100644 res/img/element-icons/send-message.svg

diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index 897167f745..8c2a997490 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -249,7 +249,7 @@ limitations under the License.
 }
 
 .mx_MessageComposer_sendMessage::before {
-    mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+    mask-image: url('$(res)/img/element-icons/send-message.svg');
 }
 
 .mx_MessageComposer_formatting {
diff --git a/res/img/element-icons/send-message.svg b/res/img/element-icons/send-message.svg
new file mode 100644
index 0000000000..2e74745e21
--- /dev/null
+++ b/res/img/element-icons/send-message.svg
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   viewBox="0 0 20 20"
+   id="vector"
+   version="1.1"
+   sodipodi:docname="send-message.svg"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
+  <metadata
+     id="metadata8">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs6" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1914"
+     inkscape:window-height="1048"
+     id="namedview4"
+     showgrid="false"
+     inkscape:zoom="22.598517"
+     inkscape:cx="7.9405039"
+     inkscape:cy="9.6825447"
+     inkscape:window-x="1920"
+     inkscape:window-y="146"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="vector" />
+  <path
+     id="path"
+     d="M 18.792 11.145 L 2.356 19.359 C 1.249 19.913 0.097 18.725 0.638 17.642 C 0.638 17.642 2.675 13.528 3.235 12.451 C 3.796 11.373 4.437 11.187 10.393 10.417 C 10.614 10.388 10.794 10.222 10.794 10 C 10.794 9.778 10.614 9.612 10.393 9.583 C 4.437 8.813 3.796 8.627 3.235 7.549 C 2.675 6.472 0.638 2.358 0.638 2.358 C 0.097 1.275 1.249 0.087 2.356 0.64 L 18.792 8.855 C 19.736 9.326 19.736 10.674 18.792 11.145 Z"
+     fill="#ffffff"
+     style="fill:#060000;fill-opacity:1" />
+</svg>

From 263f2136502c657c4dcbb07674e965576ad562ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 9 Jan 2021 09:27:02 +0100
Subject: [PATCH 037/389] Remove an empty line
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_MessageComposer.scss | 1 -
 1 file changed, 1 deletion(-)

diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index 8c2a997490..8b34318f1d 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -239,7 +239,6 @@ limitations under the License.
     mask-image: url('$(res)/img/element-icons/call/video-call.svg');
 }
 
-
 .mx_MessageComposer_emoji::before {
     mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
 }

From 7d120f7183eef575f16d78b1e9b4fdfabf7d2848 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 9 Jan 2021 09:33:11 +0100
Subject: [PATCH 038/389] Simplifie svg
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/img/element-icons/send-message.svg | 55 +-------------------------
 1 file changed, 2 insertions(+), 53 deletions(-)

diff --git a/res/img/element-icons/send-message.svg b/res/img/element-icons/send-message.svg
index 2e74745e21..ce35bf8bc8 100644
--- a/res/img/element-icons/send-message.svg
+++ b/res/img/element-icons/send-message.svg
@@ -1,54 +1,3 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   viewBox="0 0 20 20"
-   id="vector"
-   version="1.1"
-   sodipodi:docname="send-message.svg"
-   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
-  <metadata
-     id="metadata8">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <defs
-     id="defs6" />
-  <sodipodi:namedview
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1914"
-     inkscape:window-height="1048"
-     id="namedview4"
-     showgrid="false"
-     inkscape:zoom="22.598517"
-     inkscape:cx="7.9405039"
-     inkscape:cy="9.6825447"
-     inkscape:window-x="1920"
-     inkscape:window-y="146"
-     inkscape:window-maximized="0"
-     inkscape:current-layer="vector" />
-  <path
-     id="path"
-     d="M 18.792 11.145 L 2.356 19.359 C 1.249 19.913 0.097 18.725 0.638 17.642 C 0.638 17.642 2.675 13.528 3.235 12.451 C 3.796 11.373 4.437 11.187 10.393 10.417 C 10.614 10.388 10.794 10.222 10.794 10 C 10.794 9.778 10.614 9.612 10.393 9.583 C 4.437 8.813 3.796 8.627 3.235 7.549 C 2.675 6.472 0.638 2.358 0.638 2.358 C 0.097 1.275 1.249 0.087 2.356 0.64 L 18.792 8.855 C 19.736 9.326 19.736 10.674 18.792 11.145 Z"
-     fill="#ffffff"
-     style="fill:#060000;fill-opacity:1" />
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M 18.792 11.145 L 2.356 19.359 C 1.249 19.913 0.097 18.725 0.638 17.642 C 0.638 17.642 2.675 13.528 3.235 12.451 C 3.796 11.373 4.437 11.187 10.393 10.417 C 10.614 10.388 10.794 10.222 10.794 10 C 10.794 9.778 10.614 9.612 10.393 9.583 C 4.437 8.813 3.796 8.627 3.235 7.549 C 2.675 6.472 0.638 2.358 0.638 2.358 C 0.097 1.275 1.249 0.087 2.356 0.64 L 18.792 8.855 C 19.736 9.326 19.736 10.674 18.792 11.145 Z" fill="black"/>
 </svg>

From 9f1113b3bd47295d1e6f0d2e897bcb93773a06cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 16 Jan 2021 16:35:50 +0100
Subject: [PATCH 039/389] Watch setting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 315b1b78c7..a18dded04f 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -263,6 +263,7 @@ export default class MessageComposer extends React.Component {
             tombstone: this._getRoomTombstone(),
             canSendMessages: this.props.room.maySendMessage(),
             showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
+            showSendButton: SettingsStore.getValue("MessageComposerInput.sendButton"),
             hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
             joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
         };
@@ -280,6 +281,12 @@ export default class MessageComposer extends React.Component {
         }
     };
 
+    onSendButtonChanged = () => {
+        this.setState({
+            showSendButton: SettingsStore.getValue("MessageComposerInput.sendButton"),
+        });
+    }
+
     _onWidgetUpdate = () => {
         this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)});
     };
@@ -292,6 +299,8 @@ export default class MessageComposer extends React.Component {
         this.dispatcherRef = dis.register(this.onAction);
         MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
         this._waitForOwnMember();
+        this.showSendButtonRef = SettingsStore.watchSetting(
+            "MessageComposerInput.sendButton", null, this.onSendButtonChanged);
     }
 
     _waitForOwnMember() {
@@ -317,6 +326,7 @@ export default class MessageComposer extends React.Component {
         WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
         ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
         dis.unregister(this.dispatcherRef);
+        SettingsStore.unwatchSetting(this.showSendButtonRef);
     }
 
     _onRoomStateEvents(ev, state) {
@@ -455,7 +465,7 @@ export default class MessageComposer extends React.Component {
                 }
             }
 
-            if (SettingsStore.getValue("MessageComposerInput.sendButton")) {
+            if (this.state.showSendButton) {
                 controls.push((
                     <AccessibleTooltipButton
                         className="mx_MessageComposer_button mx_MessageComposer_sendMessage"

From c61e41ba77fdeedcda71d91a2b0ce0ab1fe24ea1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 16 Jan 2021 16:37:50 +0100
Subject: [PATCH 040/389] Rename setting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js               | 6 +++---
 .../views/settings/tabs/user/PreferencesUserSettingsTab.js  | 2 +-
 src/settings/Settings.ts                                    | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index a18dded04f..1e43aa6652 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -263,7 +263,7 @@ export default class MessageComposer extends React.Component {
             tombstone: this._getRoomTombstone(),
             canSendMessages: this.props.room.maySendMessage(),
             showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
-            showSendButton: SettingsStore.getValue("MessageComposerInput.sendButton"),
+            showSendButton: SettingsStore.getValue("MessageComposerInput.showSendButton"),
             hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
             joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
         };
@@ -283,7 +283,7 @@ export default class MessageComposer extends React.Component {
 
     onSendButtonChanged = () => {
         this.setState({
-            showSendButton: SettingsStore.getValue("MessageComposerInput.sendButton"),
+            showSendButton: SettingsStore.getValue("MessageComposerInput.showSendButton"),
         });
     }
 
@@ -300,7 +300,7 @@ export default class MessageComposer extends React.Component {
         MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
         this._waitForOwnMember();
         this.showSendButtonRef = SettingsStore.watchSetting(
-            "MessageComposerInput.sendButton", null, this.onSendButtonChanged);
+            "MessageComposerInput.showSendButton", null, this.onSendButtonChanged);
     }
 
     _waitForOwnMember() {
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index 31971b7167..eff0824d59 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -34,7 +34,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'MessageComposerInput.suggestEmoji',
         'sendTypingNotifications',
         'MessageComposerInput.ctrlEnterToSend',
-        `MessageComposerInput.sendButton`,
+        `MessageComposerInput.showSendButton`,
     ];
 
     static TIMELINE_SETTINGS = [
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 50d0f919e6..2d8385240c 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -336,7 +336,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
         default: false,
     },
-    "MessageComposerInput.sendButton": {
+    "MessageComposerInput.showSendButton": {
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
         displayName: _td("Show send message button"),
         default: false,

From c9f5c90047e0140da31050d97d0f831bf50858c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 16 Jan 2021 16:43:47 +0100
Subject: [PATCH 041/389] Rename method
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 1e43aa6652..9683c4c79e 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -281,7 +281,7 @@ export default class MessageComposer extends React.Component {
         }
     };
 
-    onSendButtonChanged = () => {
+    onShowSendButtonChanged = () => {
         this.setState({
             showSendButton: SettingsStore.getValue("MessageComposerInput.showSendButton"),
         });
@@ -300,7 +300,7 @@ export default class MessageComposer extends React.Component {
         MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
         this._waitForOwnMember();
         this.showSendButtonRef = SettingsStore.watchSetting(
-            "MessageComposerInput.showSendButton", null, this.onSendButtonChanged);
+            "MessageComposerInput.showSendButton", null, this.onShowSendButtonChanged);
     }
 
     _waitForOwnMember() {

From 90ad3360b628136f4e5dfc69db2c4fd81e07f0aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 24 Jan 2021 09:15:11 +0100
Subject: [PATCH 042/389] Fixed read receipts?
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_EventTile.scss     | 8 +-------
 src/components/views/rooms/EventTile.js | 6 +++---
 2 files changed, 4 insertions(+), 10 deletions(-)

diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 429ac7ed4b..e2cff70841 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -258,17 +258,11 @@ $left-gutter: 64px;
     display: inline-block;
     width: 14px;
     height: 14px;
-    top: 29px;
+    top: -20px;
     user-select: none;
     z-index: 1;
 }
 
-.mx_EventTile_continuation .mx_EventTile_readAvatars,
-.mx_EventTile_info .mx_EventTile_readAvatars,
-.mx_EventTile_emote .mx_EventTile_readAvatars {
-    top: 7px;
-}
-
 .mx_EventTile_readAvatars .mx_BaseAvatar {
     position: absolute;
     display: inline-block;
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 4df74f77ce..7a2047af70 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -950,9 +950,6 @@ export default class EventTile extends React.Component {
                 return (
                     <div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
                         { ircTimestamp }
-                        <div className="mx_EventTile_msgOption">
-                            { readAvatars }
-                        </div>
                         { sender }
                         { ircPadlock }
                         <div className="mx_EventTile_line">
@@ -971,6 +968,9 @@ export default class EventTile extends React.Component {
                             { reactionsRow }
                             { actionBar }
                         </div>
+                        <div className="mx_EventTile_msgOption">
+                            { readAvatars }
+                        </div>
                         {
                             // The avatar goes after the event tile as it's absolutely positioned to be over the
                             // event tile line, so needs to be later in the DOM so it appears on top (this avoids

From 5de92b68d954ba3f997f2d5713d954ee05303b2e Mon Sep 17 00:00:00 2001
From: Will Hunt <willh@matrix.org>
Date: Wed, 27 Jan 2021 11:39:57 +0000
Subject: [PATCH 043/389] Show a specific error for hs_disabled

---
 src/components/structures/LoggedInView.tsx      | 2 +-
 src/components/structures/RoomStatusBar.js      | 4 ++++
 src/components/structures/auth/Login.tsx        | 3 +++
 src/components/structures/auth/Registration.tsx | 1 +
 src/toasts/ServerLimitToast.tsx                 | 1 +
 src/utils/ErrorUtils.js                         | 1 +
 6 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 70ec2b7033..508b7f05e7 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -94,7 +94,7 @@ interface IProps {
 
 interface IUsageLimit {
     // eslint-disable-next-line camelcase
-    limit_type: "monthly_active_user" | string;
+    limit_type: "monthly_active_user" | "hs_disabled" | string;
     // eslint-disable-next-line camelcase
     admin_contact?: string;
 }
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index c1c4ad6292..aa4bceba74 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -195,6 +195,10 @@ export default class RoomStatusBar extends React.Component {
                     "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
                     "Please <a>contact your service administrator</a> to continue using the service.",
                 ),
+                'hs_disabled': _td(
+                    "Your message wasn't sent because this homeserver has been blocked by it's administrator. " +
+                    "Please <a>contact your service administrator</a> to continue using the service.",
+                ),
                 '': _td(
                     "Your message wasn't sent because this homeserver has exceeded a resource limit. " +
                     "Please <a>contact your service administrator</a> to continue using the service.",
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 606aeb44ab..a9fd363763 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -218,6 +218,9 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
                         'monthly_active_user': _td(
                             "This homeserver has hit its Monthly Active User limit.",
                         ),
+                        'hs_blocked': _td(
+                            "This homeserver has been blocked by it's administrator.",
+                        ),
                         '': _td(
                             "This homeserver has exceeded one of its resource limits.",
                         ),
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 095f3d3433..f9d338902c 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -276,6 +276,7 @@ export default class Registration extends React.Component<IProps, IState> {
                     response.data.admin_contact,
                     {
                         'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
+                        'hs_blocked': _td("This homeserver has been blocked by it's administrator."),
                         '': _td("This homeserver has exceeded one of its resource limits."),
                     },
                 );
diff --git a/src/toasts/ServerLimitToast.tsx b/src/toasts/ServerLimitToast.tsx
index d35140be3d..9dbe8c05f1 100644
--- a/src/toasts/ServerLimitToast.tsx
+++ b/src/toasts/ServerLimitToast.tsx
@@ -26,6 +26,7 @@ const TOAST_KEY = "serverlimit";
 export const showToast = (limitType: string, adminContact?: string, syncError?: boolean) => {
     const errorText = messageForResourceLimitError(limitType, adminContact, {
         'monthly_active_user': _td("Your homeserver has exceeded its user limit."),
+        'hs_blocked': _td("This homeserver has been blocked by it's administrator."),
         '': _td("Your homeserver has exceeded one of its resource limits."),
     });
     const contactText = messageForResourceLimitError(limitType, adminContact, {
diff --git a/src/utils/ErrorUtils.js b/src/utils/ErrorUtils.js
index f0a4d7c49e..2c6acd5503 100644
--- a/src/utils/ErrorUtils.js
+++ b/src/utils/ErrorUtils.js
@@ -62,6 +62,7 @@ export function messageForSyncError(err) {
             err.data.admin_contact,
             {
                 'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
+                'hs_blocked': _td("This homeserver has been blocked by its administrator."),
                 '': _td("This homeserver has exceeded one of its resource limits."),
             },
         );

From 27724a93d28d7945e49f94b5fa1158095bd84d8d Mon Sep 17 00:00:00 2001
From: Will Hunt <willh@matrix.org>
Date: Wed, 27 Jan 2021 11:42:36 +0000
Subject: [PATCH 044/389] new strings

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

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 8d047ea3f1..e55ab581ca 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -650,6 +650,7 @@
     "Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration",
     "The message you are trying to send is too large.": "The message you are trying to send is too large.",
     "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.",
+    "This homeserver has been blocked by its administrator.": "This homeserver has been blocked by its administrator.",
     "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.",
     "Please <a>contact your service administrator</a> to continue using the service.": "Please <a>contact your service administrator</a> to continue using the service.",
     "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...",
@@ -727,6 +728,7 @@
     "Enable desktop notifications": "Enable desktop notifications",
     "Enable": "Enable",
     "Your homeserver has exceeded its user limit.": "Your homeserver has exceeded its user limit.",
+    "This homeserver has been blocked by it's administrator.": "This homeserver has been blocked by it's administrator.",
     "Your homeserver has exceeded one of its resource limits.": "Your homeserver has exceeded one of its resource limits.",
     "Contact your <a>server admin</a>.": "Contact your <a>server admin</a>.",
     "Warning": "Warning",
@@ -2471,6 +2473,7 @@
     "Filter rooms and people": "Filter rooms and people",
     "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
     "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
+    "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.",
     "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",
     "%(count)s of your messages have not been sent.|other": "Some of your messages have not been sent.",
     "%(count)s of your messages have not been sent.|one": "Your message was not sent.",

From cc38bcf333bc9fdd7d8ebc7d0b4d06330ba7e359 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 6 Feb 2021 15:09:21 +0100
Subject: [PATCH 045/389] Display room name
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/elements/Pill.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index daa4cb70e2..f1527c48b1 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -226,7 +226,7 @@ class Pill extends React.Component {
             case Pill.TYPE_ROOM_MENTION: {
                 const room = this.state.room;
                 if (room) {
-                    linkText = resource;
+                    linkText = room.name;
                     if (this.props.shouldShowPillAvatar) {
                         avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
                     }

From ffc0230ab4eb3902bdd6e22280f78c8b0870ba54 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 6 Feb 2021 19:09:34 +0100
Subject: [PATCH 046/389] Use resource as a fallback
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This is useful mainly for the tests they would otherwise fail as there is no room name to display while the tests are run.

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/elements/Pill.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index f1527c48b1..cbbe562754 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -226,7 +226,7 @@ class Pill extends React.Component {
             case Pill.TYPE_ROOM_MENTION: {
                 const room = this.state.room;
                 if (room) {
-                    linkText = room.name;
+                    linkText = room.name || resource;
                     if (this.props.shouldShowPillAvatar) {
                         avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
                     }

From 392b47e200c91773144e68d86aaaf0e5e6dc33d2 Mon Sep 17 00:00:00 2001
From: PunitLodha <punitlodha@pm.me>
Date: Tue, 9 Feb 2021 11:39:36 +0530
Subject: [PATCH 047/389] Add email only if the verification is complete

---
 .../views/settings/account/EmailAddresses.js     | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js
index ca66d5df65..07d2133e95 100644
--- a/src/components/views/settings/account/EmailAddresses.js
+++ b/src/components/views/settings/account/EmailAddresses.js
@@ -178,19 +178,21 @@ export default class EmailAddresses extends React.Component {
         e.preventDefault();
 
         this.setState({continueDisabled: true});
-        this.state.addTask.checkEmailLinkClicked().then(() => {
-            const email = this.state.newEmailAddress;
+        this.state.addTask.checkEmailLinkClicked().then(([finished]) => {
+            if (finished) {
+                const email = this.state.newEmailAddress;
+                const emails = [
+                    ...this.props.emails,
+                    { address: email, medium: "email" },
+                ];
+                this.props.onEmailsChange(emails);
+            }
             this.setState({
                 addTask: null,
                 continueDisabled: false,
                 verifying: false,
                 newEmailAddress: "",
             });
-            const emails = [
-                ...this.props.emails,
-                { address: email, medium: "email" },
-            ];
-            this.props.onEmailsChange(emails);
         }).catch((err) => {
             this.setState({continueDisabled: false});
             const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");

From a61462bc8559815a4ea08d599268b773b681483c Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Thu, 11 Feb 2021 16:34:15 -0500
Subject: [PATCH 048/389] use the default SSSS key if the default is set

implements MSC2874
---
 src/SecurityManager.ts | 21 +++++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts
index 220320470a..11d228e7ab 100644
--- a/src/SecurityManager.ts
+++ b/src/SecurityManager.ts
@@ -98,11 +98,24 @@ async function getSecretStorageKey(
     { keys: keyInfos }: { keys: Record<string, ISecretStorageKeyInfo> },
     ssssItemName,
 ): Promise<[string, Uint8Array]> {
-    const keyInfoEntries = Object.entries(keyInfos);
-    if (keyInfoEntries.length > 1) {
-        throw new Error("Multiple storage key requests not implemented");
+    const cli = MatrixClientPeg.get();
+    let keyId = await cli.getDefaultSecretStorageKeyId();
+    let keyInfo;
+    if (keyId) {
+        // use the default SSSS key if set
+        keyInfo = keyInfos[keyId];
+        if (!keyInfo) {
+            throw new Error("Unable to use default SSSS key");
+        }
+    } else {
+        // if no default SSSS key is set, fall back to a heuristic of using the
+        // only available key, if only one key is set
+        const keyInfoEntries = Object.entries(keyInfos);
+        if (keyInfoEntries.length > 1) {
+            throw new Error("Multiple storage key requests not implemented");
+        }
+        [keyId, keyInfo] = keyInfoEntries[0];
     }
-    const [keyId, keyInfo] = keyInfoEntries[0];
 
     // Check the in-memory cache
     if (isCachingAllowed() && secretStorageKeys[keyId]) {

From 33979b335446bf16bd02815ae88de7241540ed74 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 08:18:27 +0100
Subject: [PATCH 049/389] Added a tooltip
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/elements/Pill.js | 39 +++++++++++++++++++++++++--
 1 file changed, 37 insertions(+), 2 deletions(-)

diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index cbbe562754..5b3189f389 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -26,6 +26,7 @@ import FlairStore from "../../../stores/FlairStore";
 import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {Action} from "../../../dispatcher/actions";
+import Tooltip from './Tooltip';
 
 class Pill extends React.Component {
     static roomNotifPos(text) {
@@ -68,6 +69,8 @@ class Pill extends React.Component {
         group: null,
         // The room related to the room pill
         room: null,
+        // Is the user hovering the pill
+        hover: false,
     };
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@@ -154,6 +157,18 @@ class Pill extends React.Component {
         this._unmounted = true;
     }
 
+    onMouseOver = () => {
+        this.setState({
+            hover: true,
+        });
+    };
+
+    onMouseLeave = () => {
+        this.setState({
+            hover: false,
+        });
+    };
+
     doProfileLookup(userId, member) {
         MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
             if (this._unmounted) {
@@ -256,16 +271,36 @@ class Pill extends React.Component {
         });
 
         if (this.state.pillType) {
+            const {yOffset} = this.props;
+
+            let tip;
+            if (this.state.hover) {
+                tip = <Tooltip label={resource} yOffset={yOffset} />;
+            }
+
             return <MatrixClientContext.Provider value={this._matrixClient}>
                 { this.props.inMessage ?
-                    <a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
+                    <a
+                        className={classes}
+                        href={href}
+                        onClick={onClick}
+                        data-offset-key={this.props.offsetKey}
+                        onMouseOver={this.onMouseOver}
+                        onMouseLeave={this.onMouseLeave}
+                    >
                         { avatar }
                         { linkText }
                     </a> :
-                    <span className={classes} title={resource} data-offset-key={this.props.offsetKey}>
+                    <span
+                        className={classes}
+                        data-offset-key={this.props.offsetKey}
+                        onMouseOver={this.onMouseOver}
+                        onMouseLeave={this.onMouseLeave}
+                    >
                         { avatar }
                         { linkText }
                     </span> }
+                {tip}
             </MatrixClientContext.Provider>;
         } else {
             // Deliberately render nothing if the URL isn't recognised

From a975de22efca2302aa5302b5d0142aafd235a701 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 11:27:14 +0100
Subject: [PATCH 050/389] Fixed buggy tooltip
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/elements/Pill.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index 5b3189f389..68a87d40b9 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -290,6 +290,7 @@ class Pill extends React.Component {
                     >
                         { avatar }
                         { linkText }
+                        { tip }
                     </a> :
                     <span
                         className={classes}
@@ -299,8 +300,8 @@ class Pill extends React.Component {
                     >
                         { avatar }
                         { linkText }
+                        { tip }
                     </span> }
-                {tip}
             </MatrixClientContext.Provider>;
         } else {
             // Deliberately render nothing if the URL isn't recognised

From a075568e895c0e2b3e9e83c710cbc9c510b488bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 11:34:09 +0100
Subject: [PATCH 051/389] Fixed tests
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

In the previous commits I have removed the native title/tooltip so it needs to be removed from the tests as well

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 test/components/views/messages/TextualBody-test.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js
index d5b80b6756..a596825c09 100644
--- a/test/components/views/messages/TextualBody-test.js
+++ b/test/components/views/messages/TextualBody-test.js
@@ -208,7 +208,7 @@ describe("<TextualBody />", () => {
             const content = wrapper.find(".mx_EventTile_body");
             expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
                 'Hey <span>' +
-                '<a class="mx_Pill mx_UserPill" title="@user:server">' +
+                '<a class="mx_Pill mx_UserPill">' +
                 '<img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" ' +
                 'style="width: 16px; height: 16px;" title="@member:domain.bla" alt="" aria-hidden="true">Member</a>' +
                 '</span></span>');
@@ -267,8 +267,8 @@ describe("<TextualBody />", () => {
             expect(content.html()).toBe(
                 '<span class="mx_EventTile_body markdown-body" dir="auto">' +
                 'A <span><a class="mx_Pill mx_RoomPill" href="#/room/!ZxbRYPQXDXKGmDnJNg:example.com' +
-                '?via=example.com&amp;via=bob.com" ' +
-                'title="!ZxbRYPQXDXKGmDnJNg:example.com"><img class="mx_BaseAvatar mx_BaseAvatar_image" ' +
+                '?via=example.com&amp;via=bob.com"' +
+                '><img class="mx_BaseAvatar mx_BaseAvatar_image" ' +
                 'src="mxc://avatar.url/room.png" ' +
                 'style="width: 16px; height: 16px;" alt="" aria-hidden="true">' +
                 '!ZxbRYPQXDXKGmDnJNg:example.com</a></span> with vias</span>',

From cb5237a18be0bf17eede4768f8978dfc44e7c609 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 14:21:07 +0100
Subject: [PATCH 052/389] Display room name instead of alias
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/editor/parts.ts | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/editor/parts.ts b/src/editor/parts.ts
index 8a7ccfcb7b..67f6a2c0c5 100644
--- a/src/editor/parts.ts
+++ b/src/editor/parts.ts
@@ -329,8 +329,8 @@ class NewlinePart extends BasePart implements IBasePart {
 }
 
 class RoomPillPart extends PillPart {
-    constructor(displayAlias, private room: Room) {
-        super(displayAlias, displayAlias);
+    constructor(resourceId: string, label: string, private room: Room) {
+        super(resourceId, label);
     }
 
     setAvatar(node: HTMLElement) {
@@ -357,6 +357,10 @@ class RoomPillPart extends PillPart {
 }
 
 class AtRoomPillPart extends RoomPillPart {
+    constructor(text: string, room: Room) {
+        super(text, text, room);
+    }
+
     get type(): IPillPart["type"] {
         return Type.AtRoomPill;
     }
@@ -521,7 +525,7 @@ export class PartCreator {
                        r.getAltAliases().includes(alias);
             });
         }
-        return new RoomPillPart(alias, room);
+        return new RoomPillPart(alias, room ? room.name : alias, room);
     }
 
     atRoomPill(text: string) {

From d8a9b84af94fac4321d700a9c9bfee29763e7fd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 14:28:39 +0100
Subject: [PATCH 053/389] Don't show tooltip if there is nothing to display
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

We do this because resource is undefined for @room

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/elements/Pill.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index 68a87d40b9..c6806c289e 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -274,7 +274,7 @@ class Pill extends React.Component {
             const {yOffset} = this.props;
 
             let tip;
-            if (this.state.hover) {
+            if (this.state.hover && resource) {
                 tip = <Tooltip label={resource} yOffset={yOffset} />;
             }
 

From 17f09d3b7a4eb800d0d2076cc906500aeb914cc0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 15:16:07 +0100
Subject: [PATCH 054/389] Added onIsEmptyChanged prop
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/SendMessageComposer.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 553fb44c04..99761ec8ba 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -117,6 +117,7 @@ export default class SendMessageComposer extends React.Component {
         placeholder: PropTypes.string,
         permalinkCreator: PropTypes.object.isRequired,
         replyToEvent: PropTypes.object,
+        onIsEmptyChanged: PropTypes.func,
     };
 
     static contextType = MatrixClientContext;
@@ -534,10 +535,15 @@ export default class SendMessageComposer extends React.Component {
         }
     }
 
+    onChange = () => {
+        this.props.onIsEmptyChanged(this.model.isEmpty);
+    }
+
     render() {
         return (
             <div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this._onKeyDown}>
                 <BasicMessageComposer
+                    onChange={this.onChange}
                     ref={this._setEditorRef}
                     model={this.model}
                     room={this.props.room}

From 98757bb6cfc101547de9a4170587d4972b8f5252 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 15:35:04 +0100
Subject: [PATCH 055/389] Extract send button into a function
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 26 ++++++++++++++-----
 1 file changed, 19 insertions(+), 7 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 9683c4c79e..0ea5d80c92 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -50,6 +50,22 @@ ComposerAvatar.propTypes = {
     me: PropTypes.object.isRequired,
 };
 
+function SendButton(props) {
+    return (
+        <div>
+            <AccessibleTooltipButton
+                className="mx_MessageComposer_button mx_MessageComposer_sendMessage"
+                onClick={props.onClick}
+                title={_t('Send message')}
+            />
+        </div>
+    );
+}
+
+SendButton.propTypes = {
+    onClick: PropTypes.func.isRequired,
+};
+
 function CallButton(props) {
     const onVoiceCallClick = (ev) => {
         dis.dispatch({
@@ -466,13 +482,9 @@ export default class MessageComposer extends React.Component {
             }
 
             if (this.state.showSendButton) {
-                controls.push((
-                    <AccessibleTooltipButton
-                        className="mx_MessageComposer_button mx_MessageComposer_sendMessage"
-                        onClick={this.sendMessage}
-                        title={_t('Send message')}
-                    />
-                ));
+                controls.push(
+                    <SendButton onClick={this.sendMessage} />,
+                );
             }
         } else if (this.state.tombstone) {
             const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];

From 42a48ee27d8e902e7f4835d8e221ae530af76570 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 15:39:54 +0100
Subject: [PATCH 056/389] Added composerEmpty property
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 0ea5d80c92..8c13fa4dc8 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -282,6 +282,7 @@ export default class MessageComposer extends React.Component {
             showSendButton: SettingsStore.getValue("MessageComposerInput.showSendButton"),
             hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
             joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
+            composerEmpty: true,
         };
     }
 
@@ -423,6 +424,12 @@ export default class MessageComposer extends React.Component {
         this.messageComposerInput._sendMessage();
     }
 
+    onIsEmptyChanged = (isEmpty) => {
+        this.setState({
+            composerEmpty: isEmpty,
+        });
+    }
+
     render() {
         const controls = [
             this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
@@ -448,6 +455,7 @@ export default class MessageComposer extends React.Component {
                     resizeNotifier={this.props.resizeNotifier}
                     permalinkCreator={this.props.permalinkCreator}
                     replyToEvent={this.props.replyToEvent}
+                    onIsEmptyChanged={this.onIsEmptyChanged}
                 />,
                 <UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
                 <EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,

From 50b0a78132298fb906976b4f1e2824f1195c1fb2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 15:41:43 +0100
Subject: [PATCH 057/389] Renamed composerEmpty to isComposerEmpty
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 8c13fa4dc8..7918243631 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -282,7 +282,7 @@ export default class MessageComposer extends React.Component {
             showSendButton: SettingsStore.getValue("MessageComposerInput.showSendButton"),
             hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
             joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
-            composerEmpty: true,
+            isComposerEmpty: true,
         };
     }
 
@@ -426,7 +426,7 @@ export default class MessageComposer extends React.Component {
 
     onIsEmptyChanged = (isEmpty) => {
         this.setState({
-            composerEmpty: isEmpty,
+            isComposerEmpty: isEmpty,
         });
     }
 

From 35c0cb99d04690f2465f7b4eb69aa5b006ccd7e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 15:42:30 +0100
Subject: [PATCH 058/389] Use isComposerEmpty for send button
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 7918243631..61d61b8f42 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -489,7 +489,7 @@ export default class MessageComposer extends React.Component {
                 }
             }
 
-            if (this.state.showSendButton) {
+            if (!this.state.isComposerEmpty) {
                 controls.push(
                     <SendButton onClick={this.sendMessage} />,
                 );

From ba2c68819f9d2108efb6d44ff1afe73397b4feae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 15:42:59 +0100
Subject: [PATCH 059/389] Removed showSendButton setting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js          | 10 ----------
 .../settings/tabs/user/PreferencesUserSettingsTab.js   |  1 -
 src/settings/Settings.ts                               |  5 -----
 3 files changed, 16 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 61d61b8f42..f6fc8af55d 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -279,7 +279,6 @@ export default class MessageComposer extends React.Component {
             tombstone: this._getRoomTombstone(),
             canSendMessages: this.props.room.maySendMessage(),
             showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
-            showSendButton: SettingsStore.getValue("MessageComposerInput.showSendButton"),
             hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
             joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
             isComposerEmpty: true,
@@ -298,12 +297,6 @@ export default class MessageComposer extends React.Component {
         }
     };
 
-    onShowSendButtonChanged = () => {
-        this.setState({
-            showSendButton: SettingsStore.getValue("MessageComposerInput.showSendButton"),
-        });
-    }
-
     _onWidgetUpdate = () => {
         this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)});
     };
@@ -316,8 +309,6 @@ export default class MessageComposer extends React.Component {
         this.dispatcherRef = dis.register(this.onAction);
         MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
         this._waitForOwnMember();
-        this.showSendButtonRef = SettingsStore.watchSetting(
-            "MessageComposerInput.showSendButton", null, this.onShowSendButtonChanged);
     }
 
     _waitForOwnMember() {
@@ -343,7 +334,6 @@ export default class MessageComposer extends React.Component {
         WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
         ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
         dis.unregister(this.dispatcherRef);
-        SettingsStore.unwatchSetting(this.showSendButtonRef);
     }
 
     _onRoomStateEvents(ev, state) {
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index eff0824d59..4d8493401e 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -34,7 +34,6 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'MessageComposerInput.suggestEmoji',
         'sendTypingNotifications',
         'MessageComposerInput.ctrlEnterToSend',
-        `MessageComposerInput.showSendButton`,
     ];
 
     static TIMELINE_SETTINGS = [
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 2d8385240c..b239b809fe 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -336,11 +336,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
         default: false,
     },
-    "MessageComposerInput.showSendButton": {
-        supportedLevels: LEVELS_ACCOUNT_SETTINGS,
-        displayName: _td("Show send message button"),
-        default: false,
-    },
     "MessageComposerInput.autoReplaceEmoji": {
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
         displayName: _td('Automatically replace plain text Emoji'),

From daff94ecbcdfb1592c483112289412296c8720b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 15:47:33 +0100
Subject: [PATCH 060/389] i18n
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/i18n/strings/en_EN.json | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 6baccf95de..27ec207dda 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -799,7 +799,6 @@
     "Show typing notifications": "Show typing notifications",
     "Use Command + Enter to send a message": "Use Command + Enter to send a message",
     "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
-    "Show send message button": "Show send message button",
     "Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
     "Mirror local video feed": "Mirror local video feed",
     "Enable Community Filter Panel": "Enable Community Filter Panel",
@@ -1404,6 +1403,7 @@
     "Invited": "Invited",
     "Filter room members": "Filter room members",
     "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
+    "Send message": "Send message",
     "Voice call": "Voice call",
     "Video call": "Video call",
     "Hangup": "Hangup",
@@ -1413,7 +1413,6 @@
     "Send a reply…": "Send a reply…",
     "Send an encrypted message…": "Send an encrypted message…",
     "Send a message…": "Send a message…",
-    "Send message": "Send message",
     "The conversation continues here.": "The conversation continues here.",
     "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
     "You do not have permission to post to this room": "You do not have permission to post to this room",

From b26951714938a40a73f23c77db44d4a2b98769bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 15:52:42 +0100
Subject: [PATCH 061/389] Removed wrapper
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index f6fc8af55d..a78cf323c6 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -52,13 +52,11 @@ ComposerAvatar.propTypes = {
 
 function SendButton(props) {
     return (
-        <div>
-            <AccessibleTooltipButton
-                className="mx_MessageComposer_button mx_MessageComposer_sendMessage"
-                onClick={props.onClick}
-                title={_t('Send message')}
-            />
-        </div>
+        <AccessibleTooltipButton
+            className="mx_MessageComposer_button mx_MessageComposer_sendMessage"
+            onClick={props.onClick}
+            title={_t('Send message')}
+        />
     );
 }
 

From 97f5b6920c8d25588390aa6ff2c6f9dd54b379e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 16:48:46 +0100
Subject: [PATCH 062/389] Check if the method is defined before calling it
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/SendMessageComposer.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index ca27141c02..9a14e33d05 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -538,7 +538,9 @@ export default class SendMessageComposer extends React.Component {
     }
 
     onChange = () => {
-        this.props.onIsEmptyChanged(this.model.isEmpty);
+        if (this.props.onIsEmptyChanged) {
+            this.props.onIsEmptyChanged(this.model.isEmpty);
+        }
     }
 
     render() {

From 130e4f7bfddffb48d35a4a5a5adaf090d889905b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 17:06:02 +0100
Subject: [PATCH 063/389] Added some styling
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_MessageComposer.scss     | 26 +++++++++++++++++--
 src/components/views/rooms/MessageComposer.js |  2 +-
 2 files changed, 25 insertions(+), 3 deletions(-)

diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index 8b34318f1d..c24e4912d4 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -247,8 +247,30 @@ limitations under the License.
     mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
 }
 
-.mx_MessageComposer_sendMessage::before {
-    mask-image: url('$(res)/img/element-icons/send-message.svg');
+.mx_MessageComposer_sendMessage {
+    cursor: pointer;
+    position: relative;
+    margin-right: 6px;
+    width: 32px;
+    height: 32px;
+    border-radius: 100%;
+    background-color: $button-bg-color;
+
+    &:before {
+        position: absolute;
+        height: 16px;
+        width: 16px;
+        top: 8px;
+        left: 9px;
+
+        mask-image: url('$(res)/img/element-icons/send-message.svg');
+        mask-repeat: no-repeat;
+        mask-size: contain;
+        mask-position: center;
+
+        background-color: $button-fg-color;
+        content: '';
+    }
 }
 
 .mx_MessageComposer_formatting {
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index d023d334c3..d70f273be2 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -53,7 +53,7 @@ ComposerAvatar.propTypes = {
 function SendButton(props) {
     return (
         <AccessibleTooltipButton
-            className="mx_MessageComposer_button mx_MessageComposer_sendMessage"
+            className="mx_MessageComposer_sendMessage"
             onClick={props.onClick}
             title={_t('Send message')}
         />

From 3983c15302772ebfd3bc04670bf4d3dd7b7b1762 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 12 Feb 2021 17:11:24 +0100
Subject: [PATCH 064/389] Delint
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_MessageComposer.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index c24e4912d4..2789ffdfb7 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -256,7 +256,7 @@ limitations under the License.
     border-radius: 100%;
     background-color: $button-bg-color;
 
-    &:before {
+    &::before {
         position: absolute;
         height: 16px;
         width: 16px;

From 7dc6029f19502b9a450d998db5364d39b8872d95 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sat, 13 Feb 2021 15:29:38 +0100
Subject: [PATCH 065/389] Move icons
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This tweaks the icon positions to look a bit better. Espacially with a scrollbar on the side

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_EventTile.scss | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 42df3211de..60b7bb08d8 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -531,14 +531,14 @@ $left-gutter: 64px;
     display: inline-block;
     visibility: hidden;
     cursor: pointer;
-    top: 6px;
-    right: 12px;
+    top: 8px;
+    right: 8px;
     width: 19px;
     height: 19px;
     background-color: $message-action-bar-fg-color;
 }
 .mx_EventTile_buttonBottom {
-    top: 31px;
+    top: 33px;
 }
 .mx_EventTile_copyButton {
     mask-image: url($copy-button-url);

From 74a6c1e8d887f793cdd078d8e5634e948f5cae8c Mon Sep 17 00:00:00 2001
From: PunitLodha <punitlodha@pm.me>
Date: Sun, 14 Feb 2021 18:21:12 +0530
Subject: [PATCH 066/389] Dont clear email if verification cancelled by user

---
 src/components/views/settings/account/EmailAddresses.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js
index 07d2133e95..a8de7693a9 100644
--- a/src/components/views/settings/account/EmailAddresses.js
+++ b/src/components/views/settings/account/EmailAddresses.js
@@ -179,6 +179,7 @@ export default class EmailAddresses extends React.Component {
 
         this.setState({continueDisabled: true});
         this.state.addTask.checkEmailLinkClicked().then(([finished]) => {
+            let newEmailAddress = this.state.newEmailAddress;
             if (finished) {
                 const email = this.state.newEmailAddress;
                 const emails = [
@@ -186,12 +187,13 @@ export default class EmailAddresses extends React.Component {
                     { address: email, medium: "email" },
                 ];
                 this.props.onEmailsChange(emails);
+                newEmailAddress = "";
             }
             this.setState({
                 addTask: null,
                 continueDisabled: false,
                 verifying: false,
-                newEmailAddress: "",
+                newEmailAddress,
             });
         }).catch((err) => {
             this.setState({continueDisabled: false});

From 0902936d3973581ae0464c4299d7b692ed04878f Mon Sep 17 00:00:00 2001
From: PunitLodha <punitlodha@pm.me>
Date: Sun, 14 Feb 2021 18:21:39 +0530
Subject: [PATCH 067/389] Add phone number only if verification is complete

---
 .../views/settings/account/PhoneNumbers.js     | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js
index 02e995ac45..df54b5ca1f 100644
--- a/src/components/views/settings/account/PhoneNumbers.js
+++ b/src/components/views/settings/account/PhoneNumbers.js
@@ -177,21 +177,25 @@ export default class PhoneNumbers extends React.Component {
         this.setState({continueDisabled: true});
         const token = this.state.newPhoneNumberCode;
         const address = this.state.verifyMsisdn;
-        this.state.addTask.haveMsisdnToken(token).then(() => {
+        this.state.addTask.haveMsisdnToken(token).then(([finished]) => {
+            let newPhoneNumber = this.state.newPhoneNumber;
+            if (finished) {
+                const msisdns = [
+                    ...this.props.msisdns,
+                    { address, medium: "msisdn" },
+                ];
+                this.props.onMsisdnsChange(msisdns);
+                newPhoneNumber = "";
+            }
             this.setState({
                 addTask: null,
                 continueDisabled: false,
                 verifying: false,
                 verifyMsisdn: "",
                 verifyError: null,
-                newPhoneNumber: "",
+                newPhoneNumber,
                 newPhoneNumberCode: "",
             });
-            const msisdns = [
-                ...this.props.msisdns,
-                { address, medium: "msisdn" },
-            ];
-            this.props.onMsisdnsChange(msisdns);
         }).catch((err) => {
             this.setState({continueDisabled: false});
             if (err.errcode !== 'M_THREEPID_AUTH_FAILED') {

From f343aaa05a16d7cb185eff9b4ba996fc841f2e6f Mon Sep 17 00:00:00 2001
From: Michael Weimann <mail@michael-weimann.eu>
Date: Sun, 14 Feb 2021 18:37:06 +0100
Subject: [PATCH 068/389] fix context menu padding calculation

Signed-off-by: Michael Weimann <mail@michael-weimann.eu>
---
 src/components/structures/ContextMenu.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index aab7701f26..473b90ad77 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -299,7 +299,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
             // such that it does not leave the (padded) window.
             if (contextMenuRect) {
                 const padding = 10;
-                adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height + padding);
+                adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
             }
 
             position.top = adjusted;

From 75d789bb4330b497c9cd43cbe7b38c6e1c1d4f19 Mon Sep 17 00:00:00 2001
From: waclaw66 <waclaw66@seznam.cz>
Date: Tue, 16 Feb 2021 12:07:36 +0000
Subject: [PATCH 069/389] Translated using Weblate (Czech)

Currently translated at 100.0% (2764 of 2764 strings)

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

diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index c304623544..864c6208fa 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -55,7 +55,7 @@
     "Custom Server Options": "Vlastní nastavení serveru",
     "Add a widget": "Přidat widget",
     "Accept": "Přijmout",
-    "%(targetName)s accepted an invitation.": "%(targetName)s přijal/a pozvání.",
+    "%(targetName)s accepted an invitation.": "%(targetName)s přijal(a) pozvání.",
     "Account": "Účet",
     "Access Token:": "Přístupový token:",
     "Add": "Přidat",
@@ -86,10 +86,10 @@
     "Bans user with given id": "Vykáže uživatele s daným id",
     "Cannot add any more widgets": "Nelze přidat žádné další widgety",
     "Change Password": "Změnit heslo",
-    "%(senderName)s changed their profile picture.": "%(senderName)s změnil/a svůj profilový obrázek.",
-    "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s změnil/a název místnosti na %(roomName)s.",
-    "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s odstranil/a název místnosti.",
-    "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s změnil/a téma na „%(topic)s“.",
+    "%(senderName)s changed their profile picture.": "%(senderName)s změnil(a) svůj profilový obrázek.",
+    "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s změnil(a) název místnosti na %(roomName)s.",
+    "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s odstranil(a) název místnosti.",
+    "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s změnil(a) téma na „%(topic)s“.",
     "Changes your display nickname": "Změní vaši zobrazovanou přezdívku",
     "Command error": "Chyba příkazu",
     "Commands": "Příkazy",
@@ -113,8 +113,8 @@
     "Email address": "E-mailová adresa",
     "Emoji": "Emoji",
     "Enable automatic language detection for syntax highlighting": "Zapnout automatické rozpoznávání jazyků pro zvýrazňování syntaxe",
-    "%(senderName)s ended the call.": "%(senderName)s ukončil/a hovor.",
-    "Enter passphrase": "Zadejte heslo",
+    "%(senderName)s ended the call.": "%(senderName)s ukončil(a) hovor.",
+    "Enter passphrase": "Zadejte přístupovou frázi",
     "Error decrypting attachment": "Chyba při dešifrování přílohy",
     "Error: Problem communicating with the given homeserver.": "Chyba: problém v komunikaci s daným domovským serverem.",
     "Existing Call": "Probíhající hovor",
@@ -136,18 +136,18 @@
     "Forget room": "Zapomenout místnost",
     "For security, this session has been signed out. Please sign in again.": "Z bezpečnostních důvodů bylo toto přihlášení ukončeno. Přihlašte se prosím znovu.",
     "and %(count)s others...|other": "a %(count)s další...",
-    "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s upravil/a widget %(widgetName)s",
-    "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s odstranil/a widget %(widgetName)s",
-    "%(widgetName)s widget added by %(senderName)s": "%(senderName)s přidal/a widget %(widgetName)s",
+    "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s upravil(a) widget %(widgetName)s",
+    "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s odstranil(a) widget %(widgetName)s",
+    "%(widgetName)s widget added by %(senderName)s": "%(senderName)s přidal(a) widget %(widgetName)s",
     "Automatically replace plain text Emoji": "Automaticky nahrazovat textové emoji",
     "Failed to upload image": "Obrázek se nepodařilo nahrát",
-    "%(senderName)s answered the call.": "%(senderName)s přijal/a hovor.",
+    "%(senderName)s answered the call.": "%(senderName)s přijal(a) hovor.",
     "Click to mute audio": "Klepněte pro vypnutí zvuku",
     "Failed to verify email address: make sure you clicked the link in the email": "E-mailovou adresu se nepodařilo ověřit. Přesvědčte se, že jste klepli na odkaz v e-mailové zprávě",
     "Guests cannot join this room even if explicitly invited.": "Hosté nemohou vstoupit do této místnosti, i když jsou přímo pozváni.",
     "Homeserver is": "Domovský server je",
     "Identity Server is": "Server identity je",
-    "I have verified my email address": "Ověřil/a jsem svou e-mailovou adresu",
+    "I have verified my email address": "Ověřil(a) jsem svou e-mailovou adresu",
     "Import": "Importovat",
     "Import E2E room keys": "Importovat end-to-end klíče místností",
     "Incoming call from %(name)s": "Příchozí hovor od %(name)s",
@@ -156,12 +156,12 @@
     "Incorrect username and/or password.": "Nesprávné uživatelské jméno nebo heslo.",
     "Incorrect verification code": "Nesprávný ověřovací kód",
     "Invalid Email Address": "Neplatná e-mailová adresa",
-    "%(senderName)s invited %(targetName)s.": "%(senderName)s pozval/a uživatele %(targetName)s.",
+    "%(senderName)s invited %(targetName)s.": "%(senderName)s pozval(a) uživatele %(targetName)s.",
     "Invites": "Pozvánky",
     "Invites user with given id to current room": "Pozve do aktuální místnosti uživatele s daným id",
     "Join Room": "Vstoupit do místnosti",
-    "%(targetName)s joined the room.": "%(targetName)s vstoupil/a do místnosti.",
-    "%(senderName)s kicked %(targetName)s.": "%(senderName)s vykopl/a uživatele %(targetName)s.",
+    "%(targetName)s joined the room.": "%(targetName)s vstoupil(a) do místnosti.",
+    "%(senderName)s kicked %(targetName)s.": "%(senderName)s vykopl(a) uživatele %(targetName)s.",
     "Kick": "Vykopnout",
     "Kicks user with given id": "Vykopne uživatele s daným id",
     "Last seen": "Naposledy aktivní",
@@ -184,7 +184,7 @@
     "Passwords can't be empty": "Hesla nemohou být prázdná",
     "Permissions": "Oprávnění",
     "Phone": "Telefon",
-    "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s změnil/a úroveň oprávnění o %(powerLevelDiffText)s.",
+    "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s změnil(a) úroveň oprávnění o %(powerLevelDiffText)s.",
     "Define the power level of a user": "Stanovte úroveň oprávnění uživatele",
     "Failed to change power level": "Nepodařilo se změnit úroveň oprávnění",
     "Power level must be positive integer.": "Úroveň oprávnění musí být kladné celé číslo.",
@@ -201,15 +201,15 @@
     "%(roomName)s is not accessible at this time.": "Místnost %(roomName)s není v tuto chvíli dostupná.",
     "Save": "Uložit",
     "Send Reset Email": "Poslat resetovací e-mail",
-    "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s poslal/a obrázek.",
-    "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s pozval/a uživatele %(targetDisplayName)s ke vstupu do místnosti.",
+    "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s poslal(a) obrázek.",
+    "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s pozval(a) uživatele %(targetDisplayName)s ke vstupu do místnosti.",
     "Server error": "Chyba serveru",
     "Server may be unavailable, overloaded, or search timed out :(": "Server může být nedostupný, přetížený nebo vyhledávání vypršelo :(",
     "Server may be unavailable, overloaded, or you hit a bug.": "Server může být nedostupný, přetížený nebo jste narazili na chybu.",
     "Server unavailable, overloaded, or something else went wrong.": "Server je nedostupný, přetížený nebo se něco pokazilo.",
     "Session ID": "ID sezení",
-    "%(senderName)s set a profile picture.": "%(senderName)s si nastavil/a profilový obrázek.",
-    "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s si změnil/a zobrazované jméno na %(displayName)s.",
+    "%(senderName)s set a profile picture.": "%(senderName)s si nastavil(a) profilový obrázek.",
+    "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s si změnil(a) zobrazované jméno na %(displayName)s.",
     "Show timestamps in 12 hour format (e.g. 2:30pm)": "Zobrazovat čas v 12hodinovém formátu (např. 2:30 odp.)",
     "Sign in": "Přihlásit",
     "Sign out": "Odhlásit",
@@ -235,9 +235,9 @@
     "Online": "Online",
     "Offline": "Offline",
     "Check for update": "Zkontrolovat aktualizace",
-    "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s přijal/a pozvání pro %(displayName)s.",
+    "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s přijal(a) pozvání pro %(displayName)s.",
     "Active call (%(roomName)s)": "Probíhající hovor (%(roomName)s)",
-    "%(senderName)s banned %(targetName)s.": "%(senderName)s vykázal/a uživatele %(targetName)s.",
+    "%(senderName)s banned %(targetName)s.": "%(senderName)s vykázal(a) uživatele %(targetName)s.",
     "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>.": "Nelze se připojit k domovskému serveru přes HTTP, pokud je v adresním řádku HTTPS. Buď použijte HTTPS, nebo <a>povolte nezabezpečené skripty</a>.",
     "Click here to fix": "Pro opravu klepněte zde",
     "Click to mute video": "Klepněte pro zakázání videa",
@@ -258,7 +258,7 @@
     "Unable to remove contact information": "Nepodařilo se smazat kontaktní údaje",
     "Unable to verify email address.": "Nepodařilo se ověřit e-mailovou adresu.",
     "Unban": "Přijmout zpět",
-    "%(senderName)s unbanned %(targetName)s.": "%(senderName)s přijal/a zpět uživatele %(targetName)s.",
+    "%(senderName)s unbanned %(targetName)s.": "%(senderName)s přijal(a) zpět uživatele %(targetName)s.",
     "Unable to capture screen": "Nepodařilo se zachytit obrazovku",
     "Unable to enable Notifications": "Nepodařilo se povolit oznámení",
     "unknown caller": "neznámý volající",
@@ -305,11 +305,11 @@
     "Reason": "Důvod",
     "VoIP conference started.": "VoIP konference započata.",
     "VoIP conference finished.": "VoIP konference ukončena.",
-    "%(targetName)s left the room.": "%(targetName)s opustil/a místnost.",
+    "%(targetName)s left the room.": "%(targetName)s opustil(a) místnost.",
     "You are already in a call.": "Již máte probíhající hovor.",
     "%(senderName)s requested a VoIP conference.": "Uživatel %(senderName)s požádal o VoIP konferenci.",
-    "%(senderName)s removed their profile picture.": "%(senderName)s odstranil/a svůj profilový obrázek.",
-    "%(targetName)s rejected the invitation.": "%(targetName)s odmítl/a pozvání.",
+    "%(senderName)s removed their profile picture.": "%(senderName)s odstranil(a) svůj profilový obrázek.",
+    "%(targetName)s rejected the invitation.": "%(targetName)s odmítl(a) pozvání.",
     "Communities": "Skupiny",
     "Message Pinning": "Připíchnutí zprávy",
     "Your browser does not support the required cryptography extensions": "Váš prohlížeč nepodporuje požadovaná kryptografická rozšíření",
@@ -320,20 +320,20 @@
     "Admin Tools": "Nástroje pro správce",
     "No pinned messages.": "Žádné připíchnuté zprávy.",
     "Pinned Messages": "Připíchnuté zprávy",
-    "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s odstranil/a své zobrazované jméno (%(oldDisplayName)s).",
-    "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s zrušil/a pozvání pro uživatele %(targetName)s.",
-    "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s nastavil/a viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich pozvání.",
-    "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s nastavil/a viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich vstupu.",
-    "%(senderName)s made future room history visible to all room members.": "%(senderName)s nastavil/a viditelnost budoucích zpráv v této místnosti pro všechny její členy.",
-    "%(senderName)s made future room history visible to anyone.": "%(senderName)s nastavil/a viditelnost budoucích zpráv pro kohokoliv.",
-    "%(senderName)s changed the pinned messages for the room.": "%(senderName)s změnil/a připíchnuté zprávy této místnosti.",
+    "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s odstranil(a) své zobrazované jméno (%(oldDisplayName)s).",
+    "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s zrušil(a) pozvání pro uživatele %(targetName)s.",
+    "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich pozvání.",
+    "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich vstupu.",
+    "%(senderName)s made future room history visible to all room members.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy.",
+    "%(senderName)s made future room history visible to anyone.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv pro kohokoliv.",
+    "%(senderName)s changed the pinned messages for the room.": "%(senderName)s změnil(a) připíchnuté zprávy této místnosti.",
     "Authentication check failed: incorrect password?": "Kontrola ověření selhala: špatné heslo?",
     "You need to be able to invite users to do that.": "Pro tuto akci musíte mít právo zvát uživatele.",
     "Delete Widget": "Smazat widget",
     "Error decrypting image": "Chyba při dešifrování obrázku",
     "Error decrypting video": "Chyba při dešifrování videa",
-    "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s odstranil/a avatar místnosti.",
-    "%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s změnil/a avatar místnosti na <img/>",
+    "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s odstranil(a) avatar místnosti.",
+    "%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s změnil(a) avatar místnosti na <img/>",
     "Copied!": "Zkopírováno!",
     "Failed to copy": "Nepodařilo se zkopírovat",
     "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Smazáním widgetu ho odstraníte všem uživatelům v této místnosti. Opravdu chcete tento widget smazat?",
@@ -384,9 +384,9 @@
     "Invalid community ID": "Neplatné ID skupiny",
     "'%(groupId)s' is not a valid community ID": "'%(groupId)s' není platné ID skupiny",
     "New community ID (e.g. +foo:%(localDomain)s)": "Nové ID skupiny (např. +neco:%(localDomain)s)",
-    "%(senderName)s sent an image": "%(senderName)s poslal/a obrázek",
-    "%(senderName)s sent a video": "%(senderName)s poslal/a video",
-    "%(senderName)s uploaded a file": "%(senderName)s nahrál/a soubor",
+    "%(senderName)s sent an image": "%(senderName)s poslal(a) obrázek",
+    "%(senderName)s sent a video": "%(senderName)s poslal(a) video",
+    "%(senderName)s uploaded a file": "%(senderName)s nahrál(a) soubor",
     "Disinvite this user?": "Odvolat pozvání tohoto uživatele?",
     "Kick this user?": "Vykopnout tohoto uživatele?",
     "Unban this user?": "Přijmout zpět tohoto uživatele?",
@@ -398,16 +398,16 @@
     "You have <a>disabled</a> URL previews by default.": "<a>Vypnuli</a> jste automatické náhledy webových adres.",
     "You have <a>enabled</a> URL previews by default.": "<a>Zapnuli</a> jste automatické náhledy webových adres.",
     "URL Previews": "Náhledy webových adres",
-    "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s změnil/a avatar místnosti %(roomName)s",
+    "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s změnil(a) avatar místnosti %(roomName)s",
     "Add an Integration": "Přidat začlenění",
     "An email has been sent to %(emailAddress)s": "Na adresu %(emailAddress)s jsme poslali e-mail",
     "File to import": "Soubor k importu",
-    "Passphrases must match": "Hesla se musí shodovat",
-    "Passphrase must not be empty": "Heslo nesmí být prázdné",
+    "Passphrases must match": "Přístupové fráze se musí shodovat",
+    "Passphrase must not be empty": "Přístupová fráze nesmí být prázdná",
     "Export room keys": "Exportovat klíče místnosti",
     "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Tento proces vám umožňuje exportovat do souboru klíče ke zprávám, které jste dostali v šifrovaných místnostech. Když pak tento soubor importujete do jiného Matrix klienta, všechny tyto zprávy bude možné opět dešifrovat.",
-    "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.": "Kdokoliv, kdo získá přístup k exportovanému souboru, bude moci dešifrovat všechny vaše přijaté zprávy, a proto je třeba dbát zvýšenou pozornost jeho zabezpečení. Z toho důvodu byste měli do kolonky níže zadat heslo, se kterým exportovaná data zašifrujeme. Import pak bude možný pouze se znalostí zadaného hesla.",
-    "Confirm passphrase": "Potvrďte heslo",
+    "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.": "Kdokoliv, kdo získá přístup k exportovanému souboru, bude moci dešifrovat všechny vaše přijaté zprávy, a proto je třeba dbát zvýšenou pozornost jeho zabezpečení. Z toho důvodu byste měli do kolonky níže zadat přístupovou frázi, se kterým exportovaná data zašifrujeme. Import pak bude možný pouze se znalostí zadané přístupové fráze.",
+    "Confirm passphrase": "Potvrďte přístupovou frázi",
     "Import room keys": "Importovat klíče místnosti",
     "Call Timeout": "Časový limit hovoru",
     "Show these rooms to non-members on the community page and room list?": "Zobrazovat tyto místnosti na domovské stránce skupiny a v seznamu místností i pro nečleny?",
@@ -466,18 +466,18 @@
     "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
     "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s%(count)s krát vstoupili",
     "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)svstoupili",
-    "%(oneUser)sjoined %(count)s times|one": "%(oneUser)svstoupil/a",
+    "%(oneUser)sjoined %(count)s times|one": "%(oneUser)svstoupil(a)",
     "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s %(count)s krát opustili",
     "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sopustili",
     "%(oneUser)sleft %(count)s times|other": "%(oneUser)s %(count)s krát opustil",
     "%(oneUser)sleft %(count)s times|one": "%(oneUser)sopustil",
     "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s %(count)s krát vstoupili a opustili",
     "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)svstoupili a opustili",
-    "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil/a a opustil/a",
-    "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)svstoupil/a a opustil/a",
+    "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil(a) a opustil(a)",
+    "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)svstoupil(a) a opustil(a)",
     "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s %(count)s krát opustili a znovu vstoupili",
     "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sopustili a znovu vstoupili",
-    "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s krát opustil/a a znovu vstoupil/a",
+    "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s krát opustil(a) a znovu vstoupil(a)",
     "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sopustil a znovu vstoupil",
     "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s %(count)s krát odmítli pozvání",
     "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)sodmítli pozvání",
@@ -505,12 +505,12 @@
     "was kicked %(count)s times|one": "byl vyhozen",
     "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s si %(count)s krát změnili jméno",
     "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s si změnili jméno",
-    "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s si %(count)s krát změnil/a jméno",
-    "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s si změnil/ jméno",
+    "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s si %(count)s krát změnil(a) jméno",
+    "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s si změnil(a) jméno",
     "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)ssi %(count)s krát změnili avatary",
     "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)ssi změnili avatary",
-    "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s si %(count)s krát změnil/a avatar",
-    "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s si změnil/a avatar",
+    "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s si %(count)s krát změnil(a) avatar",
+    "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s si změnil(a) avatar",
     "%(items)s and %(count)s others|other": "%(items)s a %(count)s další",
     "%(items)s and %(count)s others|one": "%(items)s a jeden další",
     "%(items)s and %(lastItem)s": "%(items)s a také %(lastItem)s",
@@ -539,7 +539,7 @@
     "To get started, please pick a username!": "Začněte tím, že si zvolíte uživatelské jméno!",
     "This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Toto bude název vašeho účtu na domovském serveru <span></span>, anebo si můžete zvolit <a>jiný server</a>.",
     "If you already have a Matrix account you can <a>log in</a> instead.": "Pokud už účet v síti Matrix máte, můžete se ihned <a>Přihlásit</a>.",
-    "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil/a",
+    "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s %(count)s krát vstoupil(a)",
     "Private Chat": "Soukromá konverzace",
     "Public Chat": "Veřejná konverzace",
     "You must <a>register</a> to use this functionality": "Pro využívání této funkce se <a>zaregistrujte</a>",
@@ -564,7 +564,7 @@
     "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Tyto místnosti se zobrazují všem členům na stránce skupiny. Členové skupiny mohou vstoupit do místnosti klepnutím.",
     "Featured Rooms:": "Hlavní místnosti:",
     "Featured Users:": "Významní uživatelé:",
-    "%(inviter)s has invited you to join this community": "%(inviter)s vás pozval/a do této skupiny",
+    "%(inviter)s has invited you to join this community": "%(inviter)s vás pozval(a) do této skupiny",
     "You are an administrator of this community": "Jste správcem této skupiny",
     "You are a member of this community": "Jste členem této skupiny",
     "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!": "Vaše skupina nemá vyplněný dlouhý popis, který je součástí HTML stránky skupiny a která se zobrazuje jejím členům.<br />Klepnutím zde otevřete nastavení, kde ho můžete doplnit!",
@@ -606,7 +606,7 @@
     "Notify the whole room": "Oznámení pro celou místnost",
     "Room Notification": "Oznámení místnosti",
     "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Tento proces vás provede importem šifrovacích klíčů, které jste si stáhli z jiného Matrix klienta. Po úspěšném naimportování budete v tomto klientovi moci dešifrovat všechny zprávy, které jste mohli dešifrovat v původním klientovi.",
-    "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Stažený soubor je chráněn heslem. Soubor můžete naimportovat pouze pokud zadáte odpovídající heslo.",
+    "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Stažený soubor je chráněn přístupovou frází. Soubor můžete naimportovat pouze pokud zadáte odpovídající přístupovou frázi.",
     "Call Failed": "Hovor selhal",
     "Send": "Odeslat",
     "collapse": "sbalit",
@@ -753,7 +753,7 @@
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
     "Missing roomId.": "Chybějící ID místnosti.",
     "Opens the Developer Tools dialog": "Otevře dialog nástrojů pro vývojáře",
-    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s si změnil/a zobrazované jméno na %(displayName)s.",
+    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s si změnil(a) zobrazované jméno na %(displayName)s.",
     "Always show encryption icons": "Vždy zobrazovat ikonu stavu šifrovaní",
     "Send analytics data": "Odesílat analytická data",
     "Enable widget screenshots on supported widgets": "Povolit screenshot widgetu pro podporované widgety",
@@ -943,14 +943,14 @@
     "Upgrades a room to a new version": "Upgraduje místnost na novou verzi",
     "This room has no topic.": "Tato místnost nemá žádné specifické téma.",
     "Sets the room name": "Nastaví název místnosti",
-    "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgradoval/a místnost.",
-    "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s zveřejnil/a místnost pro všechny s odkazem.",
-    "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s zpřístupnil/a místnost pouze na pozvání.",
-    "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s změnil/a pravidlo k připojení na %(rule)s",
-    "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s povolil/a přístup hostům.",
-    "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s zakázal/a přístup hostům.",
-    "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s změnil/a pravidlo pro přístup hostů na %(rule)s",
-    "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s nastavil/a hlavní adresu této místnosti na %(address)s.",
+    "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgradoval(a) místnost.",
+    "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s zveřejnil(a) místnost pro všechny s odkazem.",
+    "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s zpřístupnil(a) místnost pouze na pozvání.",
+    "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s změnil(a) pravidlo k připojení na %(rule)s",
+    "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s povolil(a) přístup hostům.",
+    "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s zakázal(a) přístup hostům.",
+    "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s změnil(a) pravidlo pro přístup hostů na %(rule)s",
+    "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s nastavil(a) hlavní adresu této místnosti na %(address)s.",
     "%(senderName)s removed the main address for this room.": "%(senderName)s zrušil hlavní adresu této místnosti.",
     "%(displayName)s is typing …": "%(displayName)s píše …",
     "%(names)s and %(count)s others are typing …|other": "%(names)s a %(count)s dalších píše …",
@@ -1231,10 +1231,10 @@
     "You cannot modify widgets in this room.": "V této místnosti nemůžete manipulovat s widgety.",
     "Sends the given message coloured as a rainbow": "Pošle zprávu v barvách duhy",
     "Sends the given emote coloured as a rainbow": "Pošle reakci v barvách duhy",
-    "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s přidal/a této místnosti příslušnost ke skupině %(groups)s.",
-    "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s odebral/a této místnosti příslušnost ke skupině %(groups)s.",
-    "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s přidal/a této místnosti příslušnost ke skupině %(newGroups)s a odebral/a k %(oldGroups)s.",
-    "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s zrušil/a pozvání do této místnosti pro uživatele %(targetDisplayName)s.",
+    "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s přidal(a) této místnosti příslušnost ke skupině %(groups)s.",
+    "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s odebral(a) této místnosti příslušnost ke skupině %(groups)s.",
+    "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s přidal(a) této místnosti příslušnost ke skupině %(newGroups)s a odebral(a) k %(oldGroups)s.",
+    "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s zrušil(a) pozvání do této místnosti pro uživatele %(targetDisplayName)s.",
     "No homeserver URL provided": "Nebyla zadána URL adresa domovského server",
     "Unexpected error resolving homeserver configuration": "Chyba při zjišťování konfigurace domovského serveru",
     "The user's homeserver does not support the version of the room.": "Uživatelův domovský server nepodporuje verzi této místnosti.",
@@ -1253,11 +1253,11 @@
     "Join the conversation with an account": "Připojte se ke konverzaci s účtem",
     "Sign Up": "Zaregistrovat se",
     "Sign In": "Přihlásit se",
-    "You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s vás vykopl/a z místnosti %(roomName)s",
+    "You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s vás vykopl(a) z místnosti %(roomName)s",
     "Reason: %(reason)s": "Důvod: %(reason)s",
     "Forget this room": "Zapomenout na tuto místnost",
     "Re-join": "Znovu vstoupit",
-    "You were banned from %(roomName)s by %(memberName)s": "%(memberName)s vás vykázal/a z místnosti %(roomName)s",
+    "You were banned from %(roomName)s by %(memberName)s": "%(memberName)s vás vykázal(a) z místnosti %(roomName)s",
     "Something went wrong with your invite to %(roomName)s": "S vaší pozvánkou do místnosti %(roomName)s se něco pokazilo",
     "You can only join it with a working invite.": "Vstoupit můžete jen s funkční pozvánkou.",
     "You can still join it because this is a public room.": "I přesto můžete vstoupit, protože tato místnost je veřejná.",
@@ -1265,7 +1265,7 @@
     "Try to join anyway": "Stejně se pokusit vstoupit",
     "Do you want to chat with %(user)s?": "Chcete si povídat s %(user)s?",
     "Do you want to join %(roomName)s?": "Chcete vstoupit do místnosti %(roomName)s?",
-    "<userName/> invited you": "<userName/> vás pozval/a",
+    "<userName/> invited you": "<userName/> vás pozval(a)",
     "You're previewing %(roomName)s. Want to join it?": "Nahlížíte do místnosti %(roomName)s. Chcete do ní vstoupit?",
     "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s si nelze jen tak prohlížet. Chcete do ní vstoupit?",
     "This room doesn't exist. Are you sure you're at the right place?": "Tato místnost neexistuje. Jste si jistí, že jste na správném místě?",
@@ -1280,7 +1280,7 @@
     "Invited by %(sender)s": "Pozván od uživatele %(sender)s",
     "Error updating flair": "Nepovedlo se změnit příslušnost ke skupině",
     "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Pro tuto místnost se nepovedlo změnit příslušnost ke skupině. Možná to server neumožňuje, nebo došlo k dočasné chybě.",
-    "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith> reagoval/a s %(shortName)s</reactedWith>",
+    "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith> reagoval(a) s %(shortName)s</reactedWith>",
     "edited": "upraveno",
     "Maximize apps": "Maximalizovat aplikace",
     "Rotate Left": "Otočit doleva",
@@ -1509,11 +1509,11 @@
     "Show image": "Zobrazit obrázek",
     "You verified %(name)s": "Ověřili jste %(name)s",
     "You cancelled verifying %(name)s": "Zrušili jste ověření %(name)s",
-    "%(name)s cancelled verifying": "%(name)s zrušil/a ověření",
+    "%(name)s cancelled verifying": "%(name)s zrušil(a) ověření",
     "You accepted": "Přijali jste",
-    "%(name)s accepted": "%(name)s přijal/a",
+    "%(name)s accepted": "%(name)s přijal(a)",
     "You cancelled": "Zrušili jste",
-    "%(name)s cancelled": "%(name)s zrušil/a",
+    "%(name)s cancelled": "%(name)s zrušil(a)",
     "%(name)s wants to verify": "%(name)s chce ověřit",
     "You sent a verification request": "Poslali jste požadavek na ověření",
     "Show all": "Zobrazit vše",
@@ -1532,8 +1532,8 @@
     "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Vyrobte prosím <newIssueLink>nové issue</newIssueLink> na GitHubu abychom mohli chybu opravit.",
     "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s neudělali %(count)s krát žádnou změnu",
     "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s neudělali žádnou změnu",
-    "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s neudělal/a %(count)s krát žádnou změnu",
-    "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s neudělal/a žádnou změnu",
+    "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s neudělal(a) %(count)s krát žádnou změnu",
+    "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s neudělal(a) žádnou změnu",
     "e.g. my-room": "např. moje-mistnost",
     "Use bots, bridges, widgets and sticker packs": "Použít roboty, propojení, widgety a balíky samolepek",
     "Terms of Service": "Podmínky použití",
@@ -1555,7 +1555,7 @@
     "Explore": "Procházet",
     "Filter": "Filtr místností",
     "Filter rooms…": "Najít místnost…",
-    "%(creator)s created and configured the room.": "%(creator)s vytvořil/a a nakonfiguroval/a místnost.",
+    "%(creator)s created and configured the room.": "%(creator)s vytvořil(a) a nakonfiguroval(a) místnost.",
     "Preview": "Náhled",
     "View": "Zobrazit",
     "Find a room…": "Najít místnost…",
@@ -1593,23 +1593,23 @@
     "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s spustil hovor. (není podporováno tímto prohlížečem)",
     "%(senderName)s placed a video call.": "%(senderName)s spustil videohovor.",
     "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s spustil videohovor. (není podporováno tímto prohlížečem)",
-    "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s odstranil/a pravidlo blokující uživatele odpovídající %(glob)s",
+    "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s odstranil(a) pravidlo blokující uživatele odpovídající %(glob)s",
     "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s odstranil pravidlo blokující místnosti odpovídající %(glob)s",
     "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s odstranil pravidlo blokující servery odpovídající %(glob)s",
     "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s odstranil blokující pravidlo %(glob)s",
     "%(senderName)s updated an invalid ban rule": "%(senderName)s aktualizoval neplatné pravidlo blokování",
-    "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval/a pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
+    "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval(a) pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
     "%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval pravidlo blokující místnosti odpovídající %(glob)s z důvodu %(reason)s",
     "%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval pravidlo blokující servery odpovídající %(glob)s z důvodu %(reason)s",
     "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s aktualizoval blokovací pravidlo odpovídající %(glob)s z důvodu %(reason)s",
-    "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s vytvořil/a pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
+    "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s vytvořil(a) pravidlo blokující uživatele odpovídající %(glob)s z důvodu %(reason)s",
     "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s vytvořil pravidlo blokující místnosti odpovídající %(glob)s z důvodu %(reason)s",
     "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s vytvořil pravidlo blokující servery odpovídající %(glob)s z důvodu %(reason)s",
     "%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s vytvořil blokovací pravidlo odpovídající %(glob)s z důvodu %(reason)s",
-    "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a pravidlo blokující uživatele odpovídající %(oldGlob)s na uživatele odpovídající %(newGlob)s z důvodu %(reason)s",
-    "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a pravidlo blokující místnosti odpovídající %(oldGlob)s na místnosti odpovídající %(newGlob)s z důvodu %(reason)s",
-    "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a pravidlo blokující servery odpovídající %(oldGlob)s na servery odpovídající %(newGlob)s z důvodu %(reason)s",
-    "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil/a blokovací pravidlo odpovídající %(oldGlob)s na odpovídající %(newGlob)s z důvodu %(reason)s",
+    "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil(a) pravidlo blokující uživatele odpovídající %(oldGlob)s na uživatele odpovídající %(newGlob)s z důvodu %(reason)s",
+    "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil(a) pravidlo blokující místnosti odpovídající %(oldGlob)s na místnosti odpovídající %(newGlob)s z důvodu %(reason)s",
+    "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil(a) pravidlo blokující servery odpovídající %(oldGlob)s na servery odpovídající %(newGlob)s z důvodu %(reason)s",
+    "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s změnil(a) blokovací pravidlo odpovídající %(oldGlob)s na odpovídající %(newGlob)s z důvodu %(reason)s",
     "Try out new ways to ignore people (experimental)": "Vyzkoušejte nové metody ignorování lidí (experimentální)",
     "Match system theme": "Nastavit podle vzhledu systému",
     "My Ban List": "Můj seznam zablokovaných",
@@ -1873,7 +1873,7 @@
     "New session": "Nová relace",
     "Use this session to verify your new one, granting it access to encrypted messages:": "Použijte tuto relaci pro ověření nové a udělení jí přístupu k vašim šifrovaným zprávám:",
     "If you didn’t sign in to this session, your account may be compromised.": "Pokud jste se do této nové relace nepřihlásili, váš účet může být kompromitován.",
-    "This wasn't me": "To jsem nebyl/a já",
+    "This wasn't me": "To jsem nebyl(a) já",
     "This will allow you to return to your account after signing out, and sign in on other sessions.": "Toto vám umožní se přihlásit do dalších relací a vrátit se ke svému účtu poté, co se odhlásíte.",
     "Recovery key mismatch": "Obnovovací klíč neodpovídá",
     "Incorrect recovery passphrase": "Nesprávné heslo pro obnovení",
@@ -1893,7 +1893,7 @@
     "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Varování: Vaše osobní data (včetně šifrovacích klíčů) jsou tu pořád uložena. Smažte je, pokud chcete tuto relaci zahodit, nebo se přihlaste pod jiný účet.",
     "If you cancel now, you won't complete verifying the other user.": "Pokud teď proces zrušíte, tak nebude druhý uživatel ověřen.",
     "If you cancel now, you won't complete verifying your other session.": "Pokud teď proces zrušíte, tak nebude druhá relace ověřena.",
-    "Cancel entering passphrase?": "Zrušit zadávání hesla?",
+    "Cancel entering passphrase?": "Zrušit zadávání přístupové fráze?",
     "Setting up keys": "Příprava klíčů",
     "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)su chybí nějaké komponenty, které jsou potřeba pro vyhledávání v zabezpečených místnostech. Pokud chcete s touto funkcí experimentovat, tak si pořiďte vlastní %(brand)s Desktop s <nativeLink>přidanými komponentami</nativeLink>.",
     "Subscribing to a ban list will cause you to join it!": "Odebíráním seznamu zablokovaných uživatelů se přidáte do jeho místnosti!",
@@ -1922,7 +1922,7 @@
     "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "Relace, kterou se snažíte ověřit, neumožňuje ověření QR kódem ani pomocí emoji, což je to, co %(brand)s podporuje. Zkuste použít jiného klienta.",
     "Verify by scanning": "Ověřte naskenováním",
     "You declined": "Odmítli jste",
-    "%(name)s declined": "%(name)s odmítl/a",
+    "%(name)s declined": "%(name)s odmítl(a)",
     "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Uschovejte si kopii na bezpečném místě, například ve správci hesel nebo v trezoru.",
     "Your recovery key": "Váš obnovovací klíč",
     "Copy": "Zkopírovat",
@@ -1948,7 +1948,7 @@
     "Show shortcuts to recently viewed rooms above the room list": "Zobrazovat zkratky do nedávno zobrazených místností navrchu",
     "Cancelling…": "Rušení…",
     "Your homeserver does not support cross-signing.": "Váš domovský server nepodporuje cross-signing.",
-    "Homeserver feature support:": "Funkce podporované domovským serverem:",
+    "Homeserver feature support:": "Funkce podporovaná domovským serverem:",
     "Accepting…": "Přijímání…",
     "Accepting …": "Přijímání…",
     "Declining …": "Odmítání…",
@@ -1963,14 +1963,14 @@
     "Mark all as read": "Označit vše jako přečtené",
     "Not currently indexing messages for any room.": "Aktuálně neindexujeme žádné zprávy.",
     "%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s z %(totalRooms)s",
-    "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s změnil/a jméno místnosti z %(oldRoomName)s na %(newRoomName)s.",
-    "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s přidal/a této místnosti alternativní adresy %(addresses)s.",
-    "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s přidal/a této místnosti alternativní adresu %(addresses)s.",
-    "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s odebral/a této místnosti alternativní adresy %(addresses)s.",
-    "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s odebral/a této místnosti alternativní adresu %(addresses)s.",
-    "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s změnil/a alternativní adresy této místnosti.",
-    "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s změnil/a hlavní a alternativní adresy této místnosti.",
-    "%(senderName)s changed the addresses for this room.": "%(senderName)s změnil/a adresy této místnosti.",
+    "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s změnil(a) jméno místnosti z %(oldRoomName)s na %(newRoomName)s.",
+    "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s přidal(a) této místnosti alternativní adresy %(addresses)s.",
+    "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s přidal(a) této místnosti alternativní adresu %(addresses)s.",
+    "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s odebral(a) této místnosti alternativní adresy %(addresses)s.",
+    "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s odebral(a) této místnosti alternativní adresu %(addresses)s.",
+    "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s změnil(a) alternativní adresy této místnosti.",
+    "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s změnil(a) hlavní a alternativní adresy této místnosti.",
+    "%(senderName)s changed the addresses for this room.": "%(senderName)s změnil(a) adresy této místnosti.",
     "Manually Verify by Text": "Manuální textové ověření",
     "Interactively verify by Emoji": "Interaktivní ověření s emotikonami",
     "Support adding custom themes": "Umožnit přidání vlastního vzhledu",
@@ -2044,7 +2044,7 @@
     "Start verification again from their profile.": "Proces ověření začněte znovu z profilu kontaktu.",
     "Verification timed out.": "Ověření vypršelo.",
     "You cancelled verification on your other session.": "Na druhé relace jste proces ověření zrušili.",
-    "%(displayName)s cancelled verification.": "%(displayName)s zrušil/a proces ověření.",
+    "%(displayName)s cancelled verification.": "%(displayName)s zrušil(a) proces ověření.",
     "You cancelled verification.": "Zrušili jste proces ověření.",
     "Message deleted": "Zpráva smazána",
     "Message deleted by %(name)s": "Zpráva smazána uživatelem %(name)s",
@@ -2104,7 +2104,7 @@
     "Restart": "Restartovat",
     "Upgrade your %(brand)s": "Aktualizovat %(brand)s",
     "A new version of %(brand)s is available!": "Je dostupná nová verze %(brand)su!",
-    "Are you sure you want to cancel entering passphrase?": "Chcete určitě zrušit zadávání hesla?",
+    "Are you sure you want to cancel entering passphrase?": "Chcete určitě zrušit zadávání přístupové fráze?",
     "Use your account to sign in to the latest version": "Přihlašte se za pomoci svého účtu do nejnovější verze",
     "Riot is now Element!": "Riot je nyní Element!",
     "Learn More": "Zjistit více",
@@ -2172,7 +2172,7 @@
     "%(senderName)s left the call": "%(senderName)s opustil/a hovor",
     "Call ended": "Hovor skončil",
     "You started a call": "Začali jste hovor",
-    "%(senderName)s started a call": "%(senderName)s začal/a hovor",
+    "%(senderName)s started a call": "%(senderName)s začal(a) hovor",
     "Waiting for answer": "Čekání na odpověď",
     "%(senderName)s is calling": "%(senderName)s volá",
     "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s",
@@ -2193,7 +2193,7 @@
     "Incoming call": "Příchozí hovor",
     "Your server isn't responding to some <a>requests</a>.": "Váš server neodpovídá na některé <a>požadavky</a>.",
     "Master private key:": "Hlavní soukromý klíč:",
-    "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s nemůže v prohlížeči lokálně bezpečně uložit zprávy. Použijte <desktopLink>%(brand)s Desktop</desktopLink> aby fungovalo vyhledávání v šifrovaných zprávách.",
+    "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s nemůže bezpečně ukládat šifrované zprávy lokálně v prohlížeči. Pro zobrazení šifrovaných zpráv ve výsledcích vyhledávání použijte <desktopLink>%(brand)s Desktop</desktopLink>.",
     "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Váš administrátor vypnul šifrování ve výchozím nastavení soukromých místností a přímých chatů.",
     "To link to this room, please add an address.": "Přidejte prosím místnosti adresu aby na ní šlo odkazovat.",
     "The authenticity of this encrypted message can't be guaranteed on this device.": "Pravost této šifrované zprávy nelze na tomto zařízení ověřit.",
@@ -2256,7 +2256,7 @@
     "Start a new chat": "Založit novou konverzaci",
     "Which officially provided instance you are using, if any": "Kterou oficiální instanci Riot.im používáte (a jestli vůbec)",
     "Change the topic of this room": "Změnit téma této místnosti",
-    "%(senderName)s declined the call.": "%(senderName)s odmítl/a hovor.",
+    "%(senderName)s declined the call.": "%(senderName)s odmítl(a) hovor.",
     "(an error occurred)": "(došlo k chybě)",
     "(their device couldn't start the camera / microphone)": "(zařízení druhé strany nemohlo spustit kameru / mikrofon)",
     "(connection failed)": "(spojení selhalo)",
@@ -2295,7 +2295,7 @@
     "Attach files from chat or just drag and drop them anywhere in a room.": "Připojte soubory z chatu nebo je jednoduše přetáhněte kamkoli do místnosti.",
     "No files visible in this room": "V této místnosti nejsou viditelné žádné soubory",
     "Show files": "Zobrazit soubory",
-    "%(count)s people|other": "%(count)s lidé/í",
+    "%(count)s people|other": "%(count)s lidí",
     "About": "O",
     "You’re all caught up": "Vše vyřízeno",
     "You have no visible notifications in this room.": "V této místnosti nemáte žádná viditelná oznámení.",
@@ -2333,10 +2333,10 @@
     "Switch to light mode": "Přepnout do světlého režimu",
     "User settings": "Uživatelská nastavení",
     "Community settings": "Nastavení skupiny",
-    "Confirm your recovery passphrase": "Potvrďte vaši frázi pro obnovení",
+    "Confirm your recovery passphrase": "Potvrďte vaši přístupovou frázi pro obnovení",
     "Repeat your recovery passphrase...": "Opakujte přístupovou frázi pro obnovení...",
     "Please enter your recovery passphrase a second time to confirm.": "Potvrďte prosím podruhé svou frázi pro obnovení.",
-    "Use a different passphrase?": "Použít jinou frázi?",
+    "Use a different passphrase?": "Použít jinou přístupovou frázi?",
     "Great! This recovery passphrase looks strong enough.": "Skvělé! Tato fráze pro obnovení vypadá dostatečně silně.",
     "Enter a recovery passphrase": "Zadejte frázi pro obnovení",
     "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s nebo %(usernamePassword)s",
@@ -2439,7 +2439,7 @@
     "There are two ways you can provide feedback and help us improve %(brand)s.": "Jsou dva způsoby, jak můžete poskytnout zpětnou vazbu a pomoci nám vylepšit %(brand)s.",
     "Rate %(brand)s": "Ohodnotit %(brand)s",
     "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.": "V této relaci jste již dříve používali novější verzi %(brand)s. Chcete-li tuto verzi znovu použít s šifrováním, budete se muset odhlásit a znovu přihlásit.",
-    "Homeserver": "Homeserver",
+    "Homeserver": "Domovský server",
     "Continue with %(provider)s": "Pokračovat s %(provider)s",
     "Server Options": "Možnosti serveru",
     "This version of %(brand)s does not support viewing some encrypted files": "Tato verze %(brand)s nepodporuje zobrazení některých šifrovaných souborů",
@@ -2789,7 +2789,7 @@
     "Fill Screen": "Vyplnit obrazovku",
     "Voice Call": "Hlasový hovor",
     "Video Call": "Videohovor",
-    "%(senderName)s ended the call": "%(senderName)s ukončil/a hovor",
+    "%(senderName)s ended the call": "%(senderName)s ukončil(a) hovor",
     "You ended the call": "Ukončili jste hovor",
     "New version of %(brand)s is available": "K dispozici je nová verze %(brand)s",
     "Error leaving room": "Při opouštění místnosti došlo k chybě",
@@ -2879,7 +2879,7 @@
     "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Vloží (╯°□°)╯︵ ┻━┻ na začátek zprávy",
     "Remain on your screen while running": "Při běhu zůstává na obrazovce",
     "Remain on your screen when viewing another room, when running": "Při prohlížení jiné místnosti zůstává při běhu na obrazovce",
-    "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s změnil/a seznam přístupů serveru pro tuto místnost.",
+    "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s změnil(a) seznam přístupů serveru pro tuto místnost.",
     "Offline encrypted messaging using dehydrated devices": "Offline šifrovaná komunikace pomocí dehydrovaných zařízení",
     "See emotes posted to your active room": "Prohlédněte si emoji zveřejněné ve vaší aktivní místnosti",
     "See emotes posted to this room": "Prohlédněte si emoji zveřejněné v této místnosti",
@@ -2968,5 +2968,18 @@
     "Share your screen": "Sdílejte svou obrazovku",
     "Expand code blocks by default": "Ve výchozím nastavení rozbalit bloky kódu",
     "Show line numbers in code blocks": "Zobrazit čísla řádků v blocích kódu",
-    "Recently visited rooms": "Nedávno navštívené místnosti"
+    "Recently visited rooms": "Nedávno navštívené místnosti",
+    "Upgrade to pro": "Upgradujte na profesionální verzi",
+    "Minimize dialog": "Minimalizovat dialog",
+    "Maximize dialog": "Maximalizovat dialog",
+    "%(hostSignupBrand)s Setup": "Nastavení %(hostSignupBrand)s",
+    "You should know": "Měli byste vědět",
+    "Privacy Policy": "Zásady ochrany osobních údajů",
+    "Cookie Policy": "Zásady používání souborů cookie",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Další informace najdete v našich <privacyPolicyLink />, <termsOfServiceLink /> a <cookiePolicyLink />.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Dočasné pokračování umožňuje procesu nastavení %(hostSignupBrand)s přístup k vašemu účtu za účelem načtení ověřených e-mailových adres. Tato data se neukládají.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Připojení k domovskému serveru se nezdařilo. Zavřete toto dialogové okno a zkuste to znovu.",
+    "Abort": "Přerušit",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Opravdu chcete přerušit vytváření hostitele? Proces nemůže být navázán.",
+    "Confirm abort of host creation": "Potvrďte přerušení vytváření hostitele"
 }

From 9420cc35cc885b1284701eeae7ebe555dae98425 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 17 Feb 2021 13:22:19 +0100
Subject: [PATCH 070/389] Added a key
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index d70f273be2..cd1a51245e 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -483,7 +483,7 @@ export default class MessageComposer extends React.Component {
 
             if (!this.state.isComposerEmpty) {
                 controls.push(
-                    <SendButton onClick={this.sendMessage} />,
+                    <SendButton key="controls_send" onClick={this.sendMessage} />,
                 );
             }
         } else if (this.state.tombstone) {

From a7847f25147c8a42f5dad47a4b313bd52df932bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 17 Feb 2021 13:25:53 +0100
Subject: [PATCH 071/389] onIsEmptyChanged() -> onChange()
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js     | 7 ++++---
 src/components/views/rooms/SendMessageComposer.js | 6 ++----
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index cd1a51245e..d70cb5c786 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -415,9 +415,10 @@ export default class MessageComposer extends React.Component {
         this.messageComposerInput._sendMessage();
     }
 
-    onIsEmptyChanged = (isEmpty) => {
+    onChange = () => {
+        if (!this.messageComposerInput) return;
         this.setState({
-            isComposerEmpty: isEmpty,
+            isComposerEmpty: this.messageComposerInput.model.isEmpty,
         });
     }
 
@@ -446,7 +447,7 @@ export default class MessageComposer extends React.Component {
                     resizeNotifier={this.props.resizeNotifier}
                     permalinkCreator={this.props.permalinkCreator}
                     replyToEvent={this.props.replyToEvent}
-                    onIsEmptyChanged={this.onIsEmptyChanged}
+                    onChange={this.onChange}
                 />,
                 <UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
                 <EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 9a14e33d05..657d67aee2 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -117,7 +117,7 @@ export default class SendMessageComposer extends React.Component {
         placeholder: PropTypes.string,
         permalinkCreator: PropTypes.object.isRequired,
         replyToEvent: PropTypes.object,
-        onIsEmptyChanged: PropTypes.func,
+        onChange: PropTypes.func,
     };
 
     static contextType = MatrixClientContext;
@@ -538,9 +538,7 @@ export default class SendMessageComposer extends React.Component {
     }
 
     onChange = () => {
-        if (this.props.onIsEmptyChanged) {
-            this.props.onIsEmptyChanged(this.model.isEmpty);
-        }
+        if (this.props.onChange) this.props.onChange();
     }
 
     render() {

From 86fe5f778d3eb413621d87ab720fbf32206855c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 17 Feb 2021 13:32:48 +0100
Subject: [PATCH 072/389] Use modal as a param
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/MessageComposer.js     | 5 ++---
 src/components/views/rooms/SendMessageComposer.js | 2 +-
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index d70cb5c786..819f9749e7 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -415,10 +415,9 @@ export default class MessageComposer extends React.Component {
         this.messageComposerInput._sendMessage();
     }
 
-    onChange = () => {
-        if (!this.messageComposerInput) return;
+    onChange = (model) => {
         this.setState({
-            isComposerEmpty: this.messageComposerInput.model.isEmpty,
+            isComposerEmpty: model.isEmpty,
         });
     }
 
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 657d67aee2..068627455d 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -538,7 +538,7 @@ export default class SendMessageComposer extends React.Component {
     }
 
     onChange = () => {
-        if (this.props.onChange) this.props.onChange();
+        if (this.props.onChange) this.props.onChange(this.model);
     }
 
     render() {

From 354925c2c82ca1c8567a26c43b015558b2db8990 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 18 Feb 2021 15:16:07 +0000
Subject: [PATCH 073/389] Improve TS definitions

---
 src/components/structures/ContextMenu.tsx   | 2 +-
 src/components/views/avatars/RoomAvatar.tsx | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index aab7701f26..3073397fba 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -390,7 +390,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
 }
 
 // Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
-export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
+export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">, chevronOffset = 12) => {
     const left = elementRect.right + window.pageXOffset + 3;
     let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
     top -= chevronOffset + 8; // where 8 is half the height of the chevron
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 98d69a63e7..0e16d17da9 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -13,7 +13,7 @@ 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 React, {ComponentProps} from 'react';
 import Room from 'matrix-js-sdk/src/models/room';
 import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo';
 
@@ -24,7 +24,7 @@ import Modal from '../../../Modal';
 import * as Avatar from '../../../Avatar';
 import {ResizeMethod} from "../../../Avatar";
 
-interface IProps {
+interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick">{
     // Room may be left unset here, but if it is,
     // oobData.avatarUrl should be set (else there
     // would be nowhere to get the avatar from)

From e1acf11e67a9e7a51c427eb41a8d350fccc42786 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 18 Feb 2021 15:16:59 +0000
Subject: [PATCH 074/389] Create Room Name & Topic HOCs to simplify code
 elsewhere

---
 src/components/views/elements/RoomName.tsx  | 40 ++++++++++++++
 src/components/views/elements/RoomTopic.tsx | 45 ++++++++++++++++
 src/components/views/rooms/RoomHeader.js    | 58 ++++++---------------
 3 files changed, 101 insertions(+), 42 deletions(-)
 create mode 100644 src/components/views/elements/RoomName.tsx
 create mode 100644 src/components/views/elements/RoomTopic.tsx

diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx
new file mode 100644
index 0000000000..9178155d19
--- /dev/null
+++ b/src/components/views/elements/RoomName.tsx
@@ -0,0 +1,40 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {useEffect, useState} from "react";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import {useEventEmitter} from "../../../hooks/useEventEmitter";
+
+interface IProps {
+    room: Room;
+    children?(name: string): JSX.Element;
+}
+
+const RoomName = ({ room, children }: IProps): JSX.Element => {
+    const [name, setName] = useState(room?.name);
+    useEventEmitter(room, "Room.name", () => {
+        setName(room?.name);
+    });
+    useEffect(() => {
+        setName(room?.name);
+    }, [room]);
+
+    if (children) return children(name);
+    return name || "";
+};
+
+export default RoomName;
diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx
new file mode 100644
index 0000000000..fe8aa5a83d
--- /dev/null
+++ b/src/components/views/elements/RoomTopic.tsx
@@ -0,0 +1,45 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useEffect, useState} from "react";
+import {EventType} from "matrix-js-sdk/src/@types/event";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import {useEventEmitter} from "../../../hooks/useEventEmitter";
+import {linkifyElement} from "../../../HtmlUtils";
+
+interface IProps {
+    room?: Room;
+    children?(topic: string, ref: (element: HTMLElement) => void): JSX.Element;
+}
+
+export const getTopic = room => room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
+
+const RoomTopic = ({ room, children }: IProps): JSX.Element => {
+    const [topic, setTopic] = useState(getTopic(room));
+    useEventEmitter(room.currentState, "RoomState.events", () => {
+        setTopic(getTopic(room));
+    });
+    useEffect(() => {
+        setTopic(getTopic(room));
+    }, [room]);
+
+    const ref = e => e && linkifyElement(e);
+    if (children) return children(topic, ref);
+    return <span ref={ref}>{ topic }</span>;
+};
+
+export default RoomTopic;
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 8eb8276630..93055c69f5 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -15,14 +15,13 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {createRef} from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import RateLimitedFunc from '../../../ratelimitedfunc';
 
-import { linkifyElement } from '../../../HtmlUtils';
 import {CancelButton} from './SimpleRoomHeader';
 import SettingsStore from "../../../settings/SettingsStore";
 import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
@@ -30,6 +29,8 @@ import E2EIcon from './E2EIcon';
 import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
 import {DefaultTagID} from "../../../stores/room-list/models";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import RoomTopic from "../elements/RoomTopic";
+import RoomName from "../elements/RoomName";
 
 export default class RoomHeader extends React.Component {
     static propTypes = {
@@ -52,35 +53,13 @@ export default class RoomHeader extends React.Component {
         onCancelClick: null,
     };
 
-    constructor(props) {
-        super(props);
-
-        this._topic = createRef();
-    }
-
     componentDidMount() {
         const cli = MatrixClientPeg.get();
         cli.on("RoomState.events", this._onRoomStateEvents);
         cli.on("Room.accountData", this._onRoomAccountData);
-
-        // When a room name occurs, RoomState.events is fired *before*
-        // room.name is updated. So we have to listen to Room.name as well as
-        // RoomState.events.
-        if (this.props.room) {
-            this.props.room.on("Room.name", this._onRoomNameChange);
-        }
-    }
-
-    componentDidUpdate() {
-        if (this._topic.current) {
-            linkifyElement(this._topic.current);
-        }
     }
 
     componentWillUnmount() {
-        if (this.props.room) {
-            this.props.room.removeListener("Room.name", this._onRoomNameChange);
-        }
         const cli = MatrixClientPeg.get();
         if (cli) {
             cli.removeListener("RoomState.events", this._onRoomStateEvents);
@@ -109,10 +88,6 @@ export default class RoomHeader extends React.Component {
         this.forceUpdate();
     }, 500);
 
-    _onRoomNameChange = (room) => {
-        this.forceUpdate();
-    };
-
     _hasUnreadPins() {
         const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
         if (!currentPinEvent) return false;
@@ -170,29 +145,28 @@ export default class RoomHeader extends React.Component {
             }
         }
 
-        let roomName = _t("Join Room");
+        let oobName = _t("Join Room");
         if (this.props.oobData && this.props.oobData.name) {
-            roomName = this.props.oobData.name;
-        } else if (this.props.room) {
-            roomName = this.props.room.name;
+            oobName = this.props.oobData.name;
         }
 
         const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
         const name =
             <div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
-                <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>
+                <RoomName room={this.props.room}>
+                    {(name) => {
+                        const roomName = name || oobName;
+                        return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
+                    }}
+                </RoomName>
                 { searchStatus }
             </div>;
 
-        let topic;
-        if (this.props.room) {
-            const ev = this.props.room.currentState.getStateEvents('m.room.topic', '');
-            if (ev) {
-                topic = ev.getContent().topic;
-            }
-        }
-        const topicElement =
-            <div className="mx_RoomHeader_topic" ref={this._topic} title={topic} dir="auto">{ topic }</div>;
+        const topicElement = <RoomTopic room={this.props.room}>
+            {(topic, ref) => <div className="mx_RoomHeader_topic" ref={ref} title={topic} dir="auto">
+                { topic }
+            </div>}
+        </RoomTopic>;
 
         let roomAvatar;
         if (this.props.room) {

From 0e7a731d6170861c8fc464863d5136c77cbb2c04 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 18 Feb 2021 15:19:16 +0000
Subject: [PATCH 075/389] Make use of new room canInvite helper

---
 src/components/views/rooms/MemberList.js    | 12 +-----------
 src/components/views/rooms/NewRoomIntro.tsx |  9 +--------
 2 files changed, 2 insertions(+), 19 deletions(-)

diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index 9da6e22847..495a0f0d2c 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -450,17 +450,7 @@ export default class MemberList extends React.Component {
         let inviteButton;
 
         if (room && room.getMyMembership() === 'join') {
-            // assume we can invite until proven false
-            let canInvite = true;
-
-            const plEvent = room.currentState.getStateEvents("m.room.power_levels", "");
-            const me = room.getMember(cli.getUserId());
-            if (plEvent && me) {
-                const content = plEvent.getContent();
-                if (content && content.invite > me.powerLevel) {
-                    canInvite = false;
-                }
-            }
+            const canInvite = room.canInvite(cli.getUserId());
 
             let inviteButtonText = _t("Invite to this room");
             const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 9be3d6be18..ce426a64ed 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -100,15 +100,8 @@ const NewRoomIntro = () => {
             });
         }
 
-        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) {
+        if (room.canInvite(cli.getUserId())) {
             const onInviteClick = () => {
                 dis.dispatch({ action: "view_invite", roomId });
             };

From 50252483c6ec479d7848a8f3a47769a774801824 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 18 Feb 2021 18:05:41 +0000
Subject: [PATCH 076/389] Remove stale unused components

---
 .../views/elements/RoomDirectoryButton.js     | 41 ------------
 .../views/elements/StartChatButton.js         | 40 ------------
 .../views/elements/TintableSvgButton.js       | 63 -------------------
 3 files changed, 144 deletions(-)
 delete mode 100644 src/components/views/elements/RoomDirectoryButton.js
 delete mode 100644 src/components/views/elements/StartChatButton.js
 delete mode 100644 src/components/views/elements/TintableSvgButton.js

diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js
deleted file mode 100644
index e9de6f8d15..0000000000
--- a/src/components/views/elements/RoomDirectoryButton.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
-Copyright 2017 Vector Creations Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from 'react';
-import * as sdk from '../../../index';
-import PropTypes from 'prop-types';
-import { _t } from '../../../languageHandler';
-import {Action} from "../../../dispatcher/actions";
-
-const RoomDirectoryButton = function(props) {
-    const ActionButton = sdk.getComponent('elements.ActionButton');
-    return (
-        <ActionButton action={Action.ViewRoomDirectory}
-            mouseOverAction={props.callout ? "callout_room_directory" : null}
-            label={_t("Room directory")}
-            iconPath={require("../../../../res/img/icons-directory.svg")}
-            size={props.size}
-            tooltip={props.tooltip}
-        />
-    );
-};
-
-RoomDirectoryButton.propTypes = {
-    size: PropTypes.string,
-    tooltip: PropTypes.bool,
-};
-
-export default RoomDirectoryButton;
diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js
deleted file mode 100644
index f828f8ae4d..0000000000
--- a/src/components/views/elements/StartChatButton.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-Copyright 2017 Vector Creations Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from 'react';
-import * as sdk from '../../../index';
-import PropTypes from 'prop-types';
-import { _t } from '../../../languageHandler';
-
-const StartChatButton = function(props) {
-    const ActionButton = sdk.getComponent('elements.ActionButton');
-    return (
-        <ActionButton action="view_create_chat"
-            mouseOverAction={props.callout ? "callout_start_chat" : null}
-            label={_t("Start chat")}
-            iconPath={require("../../../../res/img/icons-people.svg")}
-            size={props.size}
-            tooltip={props.tooltip}
-        />
-    );
-};
-
-StartChatButton.propTypes = {
-    size: PropTypes.string,
-    tooltip: PropTypes.bool,
-};
-
-export default StartChatButton;
diff --git a/src/components/views/elements/TintableSvgButton.js b/src/components/views/elements/TintableSvgButton.js
deleted file mode 100644
index a3f5b7db5d..0000000000
--- a/src/components/views/elements/TintableSvgButton.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
-Copyright 2017 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 TintableSvg from './TintableSvg';
-import AccessibleButton from './AccessibleButton';
-
-export default class TintableSvgButton extends React.Component {
-    constructor(props) {
-        super(props);
-    }
-
-    render() {
-        let classes = "mx_TintableSvgButton";
-        if (this.props.className) {
-            classes += " " + this.props.className;
-        }
-        return (
-            <span
-                width={this.props.width}
-                height={this.props.height}
-                className={classes}>
-                <TintableSvg
-                    src={this.props.src}
-                    width={this.props.width}
-                    height={this.props.height}
-                ></TintableSvg>
-                <AccessibleButton
-                    onClick={this.props.onClick}
-                    element='span'
-                    title={this.props.title}
-                />
-            </span>
-        );
-    }
-}
-
-TintableSvgButton.propTypes = {
-    src: PropTypes.string,
-    title: PropTypes.string,
-    className: PropTypes.string,
-    width: PropTypes.string.isRequired,
-    height: PropTypes.string.isRequired,
-    onClick: PropTypes.func,
-};
-
-TintableSvgButton.defaultProps = {
-    onClick: function() {},
-};

From 8fc244452ce2f2d40643b2ddc1d22085d62bb9ea Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 18 Feb 2021 18:06:26 +0000
Subject: [PATCH 077/389] Prevent error being thrown so that we can throw our
 own better one

---
 src/stores/room-list/algorithms/Algorithm.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts
index 25059aabe7..f709fc3ccb 100644
--- a/src/stores/room-list/algorithms/Algorithm.ts
+++ b/src/stores/room-list/algorithms/Algorithm.ts
@@ -211,7 +211,7 @@ export class Algorithm extends EventEmitter {
         }
 
         // When we do have a room though, we expect to be able to find it
-        let tag = this.roomIdsToTags[val.roomId][0];
+        let tag = this.roomIdsToTags[val.roomId]?.[0];
         if (!tag) throw new Error(`${val.roomId} does not belong to a tag and cannot be sticky`);
 
         // We specifically do NOT use the ordered rooms set as it contains the sticky room, which

From 5de99c7708c58b2808d288160ce8fb69f4f5027c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 18 Feb 2021 19:40:24 +0100
Subject: [PATCH 078/389] Fix licenses
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/settings/_SpellCheckLanguages.scss              | 3 +--
 src/components/views/elements/SpellCheckLanguagesDropdown.tsx | 3 +--
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/res/css/views/settings/_SpellCheckLanguages.scss b/res/css/views/settings/_SpellCheckLanguages.scss
index ddfa0bf9e0..bb322c983f 100644
--- a/res/css/views/settings/_SpellCheckLanguages.scss
+++ b/res/css/views/settings/_SpellCheckLanguages.scss
@@ -1,6 +1,5 @@
 /*
-Copyright 2019 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
index 53c3f310b7..029d162573 100644
--- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -1,6 +1,5 @@
 /*
-Copyright 2017 Marcel Radzio (MTRNord)
-Copyright 2017 Vector Creations Ltd.
+Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From ed02503462d93e43659bddd3280a19a0b31e26f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 18 Feb 2021 19:41:19 +0100
Subject: [PATCH 079/389] Fix one more license
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/settings/SpellCheckSettings.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx
index bfe0774570..d08f263b5f 100644
--- a/src/components/views/settings/SpellCheckSettings.tsx
+++ b/src/components/views/settings/SpellCheckSettings.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 New Vector Ltd
+Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From 2ebc1252cbbfc9731dc412947287ef5e4c9ce460 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 18 Feb 2021 19:54:54 +0100
Subject: [PATCH 080/389] Removed unnecessary functions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 .../elements/SpellCheckLanguagesDropdown.tsx  | 37 ++++++++++---------
 .../tabs/user/GeneralUserSettingsTab.js       |  6 ++-
 src/languageHandler.tsx                       | 14 -------
 3 files changed, 25 insertions(+), 32 deletions(-)

diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
index 029d162573..c647f6e410 100644
--- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -18,7 +18,7 @@ import React from 'react';
 
 import Dropdown from "../../views/elements/Dropdown"
 import * as sdk from '../../../index';
-import * as languageHandler from '../../../languageHandler';
+import PlatformPeg from "../../../PlatformPeg";
 import SettingsStore from "../../../settings/SettingsStore";
 import { _t } from "../../../languageHandler";
 
@@ -52,23 +52,26 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
     }
 
     componentDidMount() {
-        languageHandler.getAvailableSpellCheckLanguages().then((languages) => {
-            languages.sort(function(a, b) {
-                if (a < b) return -1;
-                if (a > b) return 1;
-                return 0;
-            });
-            const langs = [];
-            languages.forEach((language) => {
-                langs.push({
-                    label: language,
-                    value: language,
+        const plaf = PlatformPeg.get();
+        if (plaf) {
+            plaf.getAvailableSpellCheckLanguages().then((languages) => {
+                languages.sort(function(a, b) {
+                    if (a < b) return -1;
+                    if (a > b) return 1;
+                    return 0;
+                });
+                const langs = [];
+                languages.forEach((language) => {
+                    langs.push({
+                        label: language,
+                        value: language,
+                    })
                 })
-            })
-            this.setState({languages: langs});
-        }).catch((e) => {
-            this.setState({languages: ['en']});
-        });
+                this.setState({languages: langs});
+            }).catch((e) => {
+                this.setState({languages: ['en']});
+            });
+        }
     }
 
     _onSearchChange(search) {
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index febbcc8e36..e87dca88c8 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -188,7 +188,10 @@ export default class GeneralUserSettingsTab extends React.Component {
         SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages);
         this.setState({spellCheckLanguages: languages});
 
-        languageHandler.setSpellCheckLanguages(languages);
+        const plaf = PlatformPeg.get();
+        if (plaf) {
+            plaf.setSpellCheckLanguages(languages);
+        }
     };
 
     _onPasswordChangeError = (err) => {
@@ -402,6 +405,7 @@ export default class GeneralUserSettingsTab extends React.Component {
     render() {
         const plaf = PlatformPeg.get();
         const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
+        console.log("LOG", supportsMultiLanguageSpellCheck);
 
         const discoWarning = this.state.requiredPolicyInfo.hasTerms
             ? <img className='mx_GeneralUserSettingsTab_warningIcon'
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index 985719fce7..b61f57d4b3 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -346,20 +346,6 @@ export function setLanguage(preferredLangs: string | string[]) {
     });
 }
 
-export function setSpellCheckLanguages(preferredLangs: string[]) {
-    const plaf = PlatformPeg.get();
-    if (plaf) {
-        plaf.setSpellCheckLanguages(preferredLangs);
-    }
-}
-
-export async function getAvailableSpellCheckLanguages(): Promise<string[]> {
-    const plaf = PlatformPeg.get();
-    if (plaf) {
-        return plaf.getAvailableSpellCheckLanguages();
-    }
-}
-
 export function getAllLanguagesFromJson() {
     return getLangsJson().then((langsObject) => {
         const langs = [];

From 305d64cda88aebef8e2a0e799606224baeac4dc1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 18 Feb 2021 20:09:39 +0100
Subject: [PATCH 081/389] Removed log
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 .../views/settings/tabs/user/GeneralUserSettingsTab.js           | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index e87dca88c8..41597604e9 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -405,7 +405,6 @@ export default class GeneralUserSettingsTab extends React.Component {
     render() {
         const plaf = PlatformPeg.get();
         const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
-        console.log("LOG", supportsMultiLanguageSpellCheck);
 
         const discoWarning = this.state.requiredPolicyInfo.hasTerms
             ? <img className='mx_GeneralUserSettingsTab_warningIcon'

From 1ba512af2ad9924259c539401c238c76246adc7e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 18 Feb 2021 20:12:48 +0100
Subject: [PATCH 082/389] Use getSpellCheckLanguages() instead of a setting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/BasePlatform.ts                                   |  4 ++++
 .../settings/tabs/user/GeneralUserSettingsTab.js      | 11 ++++++++++-
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index fe655371a5..9d7077097b 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -250,6 +250,10 @@ export default abstract class BasePlatform {
 
     setSpellCheckLanguages(preferredLangs: string[]) {}
 
+    getSpellCheckLanguages(): Promise<string[]> | null {
+        return null;
+    }
+
     getAvailableSpellCheckLanguages(): Promise<string[]> | null {
         return null;
     }
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 41597604e9..3936864215 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -50,7 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
 
         this.state = {
             language: languageHandler.getCurrentLanguage(),
-            spellCheckLanguages: SettingsStore.getValue("spell-check-languages", null, false),
+            spellCheckLanguages: [],
             haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
             serverSupportsSeparateAddAndBind: null,
             idServerHasUnsignedTerms: false,
@@ -87,6 +87,15 @@ export default class GeneralUserSettingsTab extends React.Component {
         this._getThreepidState();
     }
 
+    async componentDidMount() {
+        const plaf = PlatformPeg.get();
+        if (plaf) {
+            this.setState({
+                spellCheckLanguages: await plaf.getSpellCheckLanguages(),
+            });
+        }
+    }
+
     componentWillUnmount() {
         dis.unregister(this.dispatcherRef);
     }

From 5a6e393fa2c6344a00efc14ab4eb17fc6a258a64 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 18 Feb 2021 20:13:55 +0100
Subject: [PATCH 083/389] Removed spell-check-languages
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 .../views/settings/tabs/user/GeneralUserSettingsTab.js        | 1 -
 src/settings/Settings.ts                                      | 4 ----
 2 files changed, 5 deletions(-)

diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index 3936864215..b17ab18c39 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -194,7 +194,6 @@ export default class GeneralUserSettingsTab extends React.Component {
     };
 
     _onSpellCheckLanguagesChange = (languages) => {
-        SettingsStore.setValue("spell-check-languages", null, SettingLevel.DEVICE, languages);
         this.setState({spellCheckLanguages: languages});
 
         const plaf = PlatformPeg.get();
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index b486f0fbf8..ca5e2f1d04 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -424,10 +424,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
         default: "en",
     },
-    "spell-check-languages": {
-        supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
-        default: ["en-US"],
-    },
     "breadcrumb_rooms": {
         // not really a setting
         supportedLevels: [SettingLevel.ACCOUNT],

From 52c73a7a584e7deaea0f3b28880a23a650b89113 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 18 Feb 2021 14:56:19 -0700
Subject: [PATCH 084/389] Add developer tool to explore and edit settings

---
 res/css/views/dialogs/_DevtoolsDialog.scss    |  51 ++++
 .../views/dialogs/DevtoolsDialog.js           | 285 ++++++++++++++++++
 src/i18n/strings/en_EN.json                   |  20 ++
 src/settings/SettingsStore.ts                 |   2 +-
 4 files changed, 357 insertions(+), 1 deletion(-)

diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss
index 35cb6bc7ab..7b4449eee0 100644
--- a/res/css/views/dialogs/_DevtoolsDialog.scss
+++ b/res/css/views/dialogs/_DevtoolsDialog.scss
@@ -223,3 +223,54 @@ limitations under the License.
         content: ":";
     }
 }
+
+.mx_DevTools_SettingsExplorer {
+    table {
+        width: 100%;
+        table-layout: fixed;
+        border-collapse: collapse;
+
+        th {
+            // Colour choice: first one autocomplete gave me.
+            border-bottom: 1px solid $accent-color;
+            text-align: left;
+        }
+
+        td, th {
+            width: 360px; // "feels right" number
+
+            text-overflow: ellipsis;
+            overflow: hidden;
+            white-space: nowrap;
+        }
+
+        td+td, th+th {
+            width: auto;
+        }
+
+        tr:hover {
+            // Colour choice: first one autocomplete gave me.
+            background-color: $accent-color-50pct;
+        }
+    }
+
+    .mx_DevTools_SettingsExplorer_mutable {
+        background-color: $accent-color;
+    }
+
+    .mx_DevTools_SettingsExplorer_immutable {
+        background-color: $warning-color;
+    }
+
+    .mx_DevTools_SettingsExplorer_edit {
+        float: right;
+        margin-right: 16px;
+    }
+
+    .mx_DevTools_SettingsExplorer_warning {
+        border: 2px solid $warning-color;
+        border-radius: 4px;
+        padding: 4px;
+        margin-bottom: 8px;
+    }
+}
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index 7d4e3b462f..5b1ed86adb 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -34,6 +34,10 @@ import {
 } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 import WidgetStore from "../../../stores/WidgetStore";
 import {UPDATE_EVENT} from "../../../stores/AsyncStore";
+import {SETTINGS} from "../../../settings/Settings";
+import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
+import Modal from "../../../Modal";
+import ErrorDialog from "./ErrorDialog";
 
 class GenericEditor extends React.PureComponent {
     // static propTypes = {onBack: PropTypes.func.isRequired};
@@ -794,6 +798,286 @@ class WidgetExplorer extends React.Component {
     }
 }
 
+class SettingsExplorer extends React.Component {
+    static getLabel() {
+        return _t("Settings Explorer");
+    }
+
+    constructor(props) {
+        super(props);
+
+        this.state = {
+            query: '',
+            editSetting: null, // set to a setting ID when editing
+            viewSetting: null, // set to a setting ID when exploring in detail
+
+            explicitValues: null, // stringified JSON for edit view
+            explicitRoomValues: null, // stringified JSON for edit view
+        };
+    }
+
+    onQueryChange = (ev) => {
+        this.setState({query: ev.target.value});
+    };
+
+    onExplValuesEdit = (ev) => {
+        this.setState({explicitValues: ev.target.value});
+    };
+
+    onExplRoomValuesEdit = (ev) => {
+        this.setState({explicitRoomValues: ev.target.value});
+    };
+
+    onBack = () => {
+        if (this.state.editSetting) {
+            this.setState({editSetting: null});
+        } else if (this.state.viewSetting) {
+            this.setState({viewSetting: null});
+        } else {
+            this.props.onBack();
+        }
+    };
+
+    onViewClick = (ev, settingId) => {
+        ev.preventDefault();
+        this.setState({viewSetting: settingId});
+    };
+
+    onEditClick = (ev, settingId) => {
+        ev.preventDefault();
+        this.setState({
+            editSetting: settingId,
+            explicitValues: this.renderExplicitSettingValues(settingId, null),
+            explicitRoomValues: this.renderExplicitSettingValues(settingId, this.props.room.roomId),
+        });
+    };
+
+    onSaveClick = async () => {
+        try {
+            const settingId = this.state.editSetting;
+            const parsedExplicit = JSON.parse(this.state.explicitValues);
+            const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues);
+            for (const level of Object.keys(parsedExplicit)) {
+                console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
+                try {
+                    const val = parsedExplicit[level];
+                    await SettingsStore.setValue(settingId, null, level, val);
+                } catch (e) {
+                    console.warn(e);
+                }
+            }
+            const roomId = this.props.room.roomId;
+            for (const level of Object.keys(parsedExplicit)) {
+                console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
+                try {
+                    const val = parsedExplicitRoom[level];
+                    await SettingsStore.setValue(settingId, roomId, level, val);
+                } catch (e) {
+                    console.warn(e);
+                }
+            }
+            this.setState({
+                viewSetting: settingId,
+                editSetting: null,
+            });
+        } catch (e) {
+            Modal.createTrackedDialog('Devtools - Failed to save settings', '', ErrorDialog, {
+                title: _t("Failed to save settings"),
+                description: e.message,
+            });
+        }
+    };
+
+    renderSettingValue(val) {
+        // Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
+        const toStringTypes = ['boolean', 'number'];
+        if (toStringTypes.includes(typeof(val))) {
+            return val.toString();
+        } else {
+            return JSON.stringify(val);
+        }
+    }
+
+    renderExplicitSettingValues(setting, roomId) {
+        const vals = {};
+        for (const level of LEVEL_ORDER) {
+            try {
+                vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true);
+                if (vals[level] === undefined) {
+                    vals[level] = null;
+                }
+            } catch (e) {
+                console.warn(e);
+            }
+        }
+        return JSON.stringify(vals, null, 4);
+    }
+
+    renderCanEditLevel(roomId, level) {
+        let canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
+        const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
+        return <td className={className}><code>{canEdit.toString()}</code></td>;
+    }
+
+    render() {
+        const room = this.props.room;
+
+        if (!this.state.viewSetting && !this.state.editSetting) {
+            // view all settings
+            const allSettings = Object.keys(SETTINGS)
+                .filter(n => this.state.query ? n.toLowerCase().includes(this.state.query.toLowerCase()) : true);
+            return (
+                <div>
+                    <div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
+                        <Field
+                            label={_t('Filter results')} autoFocus={true} size={64}
+                            type="text" autoComplete="off" value={this.state.query} onChange={this.onQueryChange}
+                            className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
+                        />
+                        <table>
+                            <thead>
+                            <tr>
+                                <th>{_t("Setting ID")}</th>
+                                <th>{_t("Value")}</th>
+                                <th>{_t("Value in this room")}</th>
+                            </tr>
+                            </thead>
+                            <tbody>
+                            {allSettings.map(i => (
+                                <tr key={i}>
+                                    <td>
+                                        <a href="" onClick={(e) => this.onViewClick(e, i)}>
+                                            <code>{i}</code>
+                                        </a>
+                                        <a href="" onClick={(e) => this.onEditClick(e, i)}
+                                           className='mx_DevTools_SettingsExplorer_edit'
+                                        >
+                                            ✏
+                                        </a>
+                                    </td>
+                                    <td>
+                                        <code>{this.renderSettingValue(SettingsStore.getValue(i))}</code>
+                                    </td>
+                                    <td>
+                                        <code>
+                                            {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))}
+                                        </code>
+                                    </td>
+                                </tr>
+                            ))}
+                            </tbody>
+                        </table>
+                    </div>
+                    <div className="mx_Dialog_buttons">
+                        <button onClick={this.onBack}>{_t("Back")}</button>
+                    </div>
+                </div>
+            );
+        } else if (this.state.editSetting) {
+            return (
+                <div>
+                    <div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
+                        <h3>{_t("Setting:")} <code>{this.state.editSetting}</code></h3>
+
+                        <div className='mx_DevTools_SettingsExplorer_warning'>
+                            <b>{_t("Caution:")}</b> {_t(
+                                "This UI does NOT check the types of the values. Use at your own risk.",
+                            )}
+                        </div>
+
+                        <div>
+                            {_t("Setting definition:")}
+                            <pre><code>{JSON.stringify(SETTINGS[this.state.editSetting], null, 4)}</code></pre>
+                        </div>
+
+                        <div>
+                            <table>
+                                <thead>
+                                <tr>
+                                    <th>{_t("Level")}</th>
+                                    <th>{_t("Settable at global")}</th>
+                                    <th>{_t("Settable at room")}</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                    {LEVEL_ORDER.map(lvl => (
+                                        <tr key={lvl}>
+                                            <td><code>{lvl}</code></td>
+                                            {this.renderCanEditLevel(null, lvl)}
+                                            {this.renderCanEditLevel(room.roomId, lvl)}
+                                        </tr>
+                                    ))}
+                                </tbody>
+                            </table>
+                        </div>
+
+                        <div>
+                            <Field
+                                id="valExpl" label={_t("Values at explicit levels")} type="text"
+                                className="mx_DevTools_textarea" element="textarea"
+                                autoComplete="off" value={this.state.explicitValues}
+                                onChange={this.onExplValuesEdit}
+                            />
+                        </div>
+
+                        <div>
+                            <Field
+                                id="valExpl" label={_t("Values at explicit levels in this room")} type="text"
+                                className="mx_DevTools_textarea" element="textarea"
+                                autoComplete="off" value={this.state.explicitRoomValues}
+                                onChange={this.onExplRoomValuesEdit}
+                            />
+                        </div>
+
+                    </div>
+                    <div className="mx_Dialog_buttons">
+                        <button onClick={this.onSaveClick}>{_t("Save setting values")}</button>
+                        <button onClick={this.onBack}>{_t("Back")}</button>
+                    </div>
+                </div>
+            );
+        } else if (this.state.viewSetting) {
+            return (
+                <div>
+                    <div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
+                        <h3>{_t("Setting:")} <code>{this.state.viewSetting}</code></h3>
+
+                        <div>
+                            {_t("Setting definition:")}
+                            <pre><code>{JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}</code></pre>
+                        </div>
+
+                        <div>
+                            {_t("Value:")}&nbsp;
+                            <code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}</code>
+                        </div>
+
+                        <div>
+                            {_t("Value in this room:")}&nbsp;
+                            <code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))}</code>
+                        </div>
+
+                        <div>
+                            {_t("Values at explicit levels:")}
+                            <pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, null)}</code></pre>
+                        </div>
+
+                        <div>
+                            {_t("Values at explicit levels in this room:")}
+                            <pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}</code></pre>
+                        </div>
+
+                    </div>
+                    <div className="mx_Dialog_buttons">
+                        <button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{_t("Edit Values")}</button>
+                        <button onClick={this.onBack}>{_t("Back")}</button>
+                    </div>
+                </div>
+            );
+        }
+    }
+}
+
 const Entries = [
     SendCustomEvent,
     RoomStateExplorer,
@@ -802,6 +1086,7 @@ const Entries = [
     ServersInRoomList,
     VerificationExplorer,
     WidgetExplorer,
+    SettingsExplorer,
 ];
 
 export default class DevtoolsDialog extends React.PureComponent {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5bbbdf60b5..54f0f66eb4 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2068,6 +2068,26 @@
     "Verification Requests": "Verification Requests",
     "Active Widgets": "Active Widgets",
     "There was an error finding this widget.": "There was an error finding this widget.",
+    "Settings Explorer": "Settings Explorer",
+    "Failed to save settings": "Failed to save settings",
+    "Setting ID": "Setting ID",
+    "Value": "Value",
+    "Value in this room": "Value in this room",
+    "Setting:": "Setting:",
+    "Caution:": "Caution:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "This UI does NOT check the types of the values. Use at your own risk.",
+    "Setting definition:": "Setting definition:",
+    "Level": "Level",
+    "Settable at global": "Settable at global",
+    "Settable at room": "Settable at room",
+    "Values at explicit levels": "Values at explicit levels",
+    "Values at explicit levels in this room": "Values at explicit levels in this room",
+    "Save setting values": "Save setting values",
+    "Value:": "Value:",
+    "Value in this room:": "Value in this room:",
+    "Values at explicit levels:": "Values at explicit levels:",
+    "Values at explicit levels in this room:": "Values at explicit levels in this room:",
+    "Edit Values": "Edit Values",
     "Toolbox": "Toolbox",
     "Developer Tools": "Developer Tools",
     "There was an error updating your community. The server is unable to process your request.": "There was an error updating your community. The server is unable to process your request.",
diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts
index 6dc2a76ae8..c2675bd8f8 100644
--- a/src/settings/SettingsStore.ts
+++ b/src/settings/SettingsStore.ts
@@ -61,7 +61,7 @@ for (const key of Object.keys(LEVEL_HANDLERS)) {
     LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]);
 }
 
-const LEVEL_ORDER = [
+export const LEVEL_ORDER = [
     SettingLevel.DEVICE,
     SettingLevel.ROOM_DEVICE,
     SettingLevel.ROOM_ACCOUNT,

From 10b0051c02eec14fa48f400e3e3023d8614353f5 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 18 Feb 2021 15:30:03 -0700
Subject: [PATCH 085/389] Appease the linter

---
 res/css/views/dialogs/_DevtoolsDialog.scss     | 2 +-
 src/components/views/dialogs/DevtoolsDialog.js | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss
index 7b4449eee0..8fee740016 100644
--- a/res/css/views/dialogs/_DevtoolsDialog.scss
+++ b/res/css/views/dialogs/_DevtoolsDialog.scss
@@ -244,7 +244,7 @@ limitations under the License.
             white-space: nowrap;
         }
 
-        td+td, th+th {
+        td + td, th + th {
             width: auto;
         }
 
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index 5b1ed86adb..814378bb51 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -914,7 +914,7 @@ class SettingsExplorer extends React.Component {
     }
 
     renderCanEditLevel(roomId, level) {
-        let canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
+        const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
         const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
         return <td className={className}><code>{canEdit.toString()}</code></td>;
     }

From 3ca5632f6a68f0b57c8c642d9b374d4bea6cd788 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 00:00:10 +0000
Subject: [PATCH 086/389] Replace ObjectUtils.js with objects.ts

---
 src/ObjectUtils.js                         | 113 ---------------------
 src/components/structures/RoomView.tsx     |   5 +-
 src/components/structures/TimelinePanel.js |   6 +-
 src/components/views/rooms/AuxPanel.tsx    |   5 +-
 src/components/views/rooms/EventTile.js    |   4 +-
 src/utils/objects.ts                       |   1 +
 6 files changed, 10 insertions(+), 124 deletions(-)
 delete mode 100644 src/ObjectUtils.js

diff --git a/src/ObjectUtils.js b/src/ObjectUtils.js
deleted file mode 100644
index 24dfe61d68..0000000000
--- a/src/ObjectUtils.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
-Copyright 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.
-*/
-
-/**
- * For two objects of the form { key: [val1, val2, val3] }, work out the added/removed
- * values. Entirely new keys will result in the entire value array being added.
- * @param {Object} before
- * @param {Object} after
- * @return {Object[]} An array of objects with the form:
- * { key: $KEY, val: $VALUE, place: "add|del" }
- */
-export function getKeyValueArrayDiffs(before, after) {
-    const results = [];
-    const delta = {};
-    Object.keys(before).forEach(function(beforeKey) {
-        delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially
-        delta[beforeKey]--; // keys present in the past have -ve values
-    });
-    Object.keys(after).forEach(function(afterKey) {
-        delta[afterKey] = delta[afterKey] || 0; // init to 0 initially
-        delta[afterKey]++; // keys present in the future have +ve values
-    });
-
-    Object.keys(delta).forEach(function(muxedKey) {
-        switch (delta[muxedKey]) {
-            case 1: // A new key in after
-                after[muxedKey].forEach(function(afterVal) {
-                    results.push({ place: "add", key: muxedKey, val: afterVal });
-                });
-                break;
-            case -1: // A before key was removed
-                before[muxedKey].forEach(function(beforeVal) {
-                    results.push({ place: "del", key: muxedKey, val: beforeVal });
-                });
-                break;
-            case 0: {// A mix of added/removed keys
-                // compare old & new vals
-                const itemDelta = {};
-                before[muxedKey].forEach(function(beforeVal) {
-                    itemDelta[beforeVal] = itemDelta[beforeVal] || 0;
-                    itemDelta[beforeVal]--;
-                });
-                after[muxedKey].forEach(function(afterVal) {
-                    itemDelta[afterVal] = itemDelta[afterVal] || 0;
-                    itemDelta[afterVal]++;
-                });
-
-                Object.keys(itemDelta).forEach(function(item) {
-                    if (itemDelta[item] === 1) {
-                        results.push({ place: "add", key: muxedKey, val: item });
-                    } else if (itemDelta[item] === -1) {
-                        results.push({ place: "del", key: muxedKey, val: item });
-                    } else {
-                        // itemDelta of 0 means it was unchanged between before/after
-                    }
-                });
-                break;
-            }
-            default:
-                console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!");
-                break;
-        }
-    });
-
-    return results;
-}
-
-/**
- * Shallow-compare two objects for equality: each key and value must be identical
- * @param {Object} objA First object to compare against the second
- * @param {Object} objB Second object to compare against the first
- * @return {boolean} whether the two objects have same key=values
- */
-export function shallowEqual(objA, objB) {
-    if (objA === objB) {
-        return true;
-    }
-
-    if (typeof objA !== 'object' || objA === null ||
-          typeof objB !== 'object' || objB === null) {
-        return false;
-    }
-
-    const keysA = Object.keys(objA);
-    const keysB = Object.keys(objB);
-
-    if (keysA.length !== keysB.length) {
-        return false;
-    }
-
-    for (let i = 0; i < keysA.length; i++) {
-        const key = keysA[i];
-        if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
-            return false;
-        }
-    }
-
-    return true;
-}
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index ee50686123..68ab3c6e0c 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -38,7 +38,6 @@ import CallHandler from '../../CallHandler';
 import dis from '../../dispatcher/dispatcher';
 import Tinter from '../../Tinter';
 import rateLimitedFunc from '../../ratelimitedfunc';
-import * as ObjectUtils from '../../ObjectUtils';
 import * as Rooms from '../../Rooms';
 import eventSearch, { searchPagination } from '../../Searching';
 import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key } from '../../Keyboard';
@@ -80,6 +79,7 @@ import Notifier from "../../Notifier";
 import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
 import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
 import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
+import { objectHasDiff } from "../../utils/objects";
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -523,8 +523,7 @@ export default class RoomView extends React.Component<IProps, IState> {
     }
 
     shouldComponentUpdate(nextProps, nextState) {
-        return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
-                !ObjectUtils.shallowEqual(this.state, nextState));
+        return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState));
     }
 
     componentDidUpdate() {
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index e8da5c42d0..6bc1f70ba1 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -26,7 +26,6 @@ import {EventTimeline} from "matrix-js-sdk";
 import * as Matrix from "matrix-js-sdk";
 import { _t } from '../../languageHandler';
 import {MatrixClientPeg} from "../../MatrixClientPeg";
-import * as ObjectUtils from "../../ObjectUtils";
 import UserActivity from "../../UserActivity";
 import Modal from "../../Modal";
 import dis from "../../dispatcher/dispatcher";
@@ -37,6 +36,7 @@ import shouldHideEvent from '../../shouldHideEvent';
 import EditorStateTransfer from '../../utils/EditorStateTransfer';
 import {haveTileForEvent} from "../views/rooms/EventTile";
 import {UIFeature} from "../../settings/UIFeature";
+import {objectHasDiff} from "../../utils/objects";
 
 const PAGINATE_SIZE = 20;
 const INITIAL_SIZE = 20;
@@ -261,7 +261,7 @@ class TimelinePanel extends React.Component {
     }
 
     shouldComponentUpdate(nextProps, nextState) {
-        if (!ObjectUtils.shallowEqual(this.props, nextProps)) {
+        if (objectHasDiff(this.props, nextProps)) {
             if (DEBUG) {
                 console.group("Timeline.shouldComponentUpdate: props change");
                 console.log("props before:", this.props);
@@ -271,7 +271,7 @@ class TimelinePanel extends React.Component {
             return true;
         }
 
-        if (!ObjectUtils.shallowEqual(this.state, nextState)) {
+        if (objectHasDiff(this.state, nextState)) {
             if (DEBUG) {
                 console.group("Timeline.shouldComponentUpdate: state change");
                 console.log("state before:", this.state);
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 7966643084..4ce31be410 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -19,7 +19,6 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import { Room } from 'matrix-js-sdk/src/models/room'
 import * as sdk from '../../../index';
 import dis from "../../../dispatcher/dispatcher";
-import * as ObjectUtils from '../../../ObjectUtils';
 import AppsDrawer from './AppsDrawer';
 import { _t } from '../../../languageHandler';
 import classNames from 'classnames';
@@ -29,6 +28,7 @@ import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 import {UIFeature} from "../../../settings/UIFeature";
 import { ResizeNotifier } from "../../../utils/ResizeNotifier";
 import CallViewForRoom from '../voip/CallViewForRoom';
+import {objectHasDiff} from "../../../utils/objects";
 
 interface IProps {
     // js-sdk room object
@@ -89,8 +89,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
     }
 
     shouldComponentUpdate(nextProps, nextState) {
-        return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
-                !ObjectUtils.shallowEqual(this.state, nextState));
+        return objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState);
     }
 
     componentDidUpdate(prevProps, prevState) {
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index c856919f5a..210f966d1e 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -32,13 +32,13 @@ import {EventStatus} from 'matrix-js-sdk';
 import {formatTime} from "../../../DateUtils";
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
-import * as ObjectUtils from "../../../ObjectUtils";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {E2E_STATE} from "./E2EIcon";
 import {toRem} from "../../../utils/units";
 import {WidgetType} from "../../../widgets/WidgetType";
 import RoomAvatar from "../avatars/RoomAvatar";
 import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
+import {objectHasDiff} from "../../../utils/objects";
 
 const eventTileTypes = {
     'm.room.message': 'messages.MessageEvent',
@@ -294,7 +294,7 @@ export default class EventTile extends React.Component {
     }
 
     shouldComponentUpdate(nextProps, nextState) {
-        if (!ObjectUtils.shallowEqual(this.state, nextState)) {
+        if (objectHasDiff(this.state, nextState)) {
             return true;
         }
 
diff --git a/src/utils/objects.ts b/src/utils/objects.ts
index bc74ab9ee0..fe010df2b9 100644
--- a/src/utils/objects.ts
+++ b/src/utils/objects.ts
@@ -86,6 +86,7 @@ export function objectShallowClone<O extends {}>(a: O, propertyCloner?: (k: keyo
  * @returns True if there's a difference between the objects, false otherwise
  */
 export function objectHasDiff<O extends {}>(a: O, b: O): boolean {
+    if (a === b) return false;
     const aKeys = Object.keys(a);
     const bKeys = Object.keys(b);
     if (arrayHasDiff(aKeys, bKeys)) return true;

From 3c52446205e420689eab66af20e1f220c387dd5d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 00:01:07 +0000
Subject: [PATCH 087/389] Remove redundant PhasedRollOut

---
 src/PhasedRollOut.js       | 51 ---------------------------
 test/PhasedRollOut-test.js | 71 --------------------------------------
 2 files changed, 122 deletions(-)
 delete mode 100644 src/PhasedRollOut.js
 delete mode 100644 test/PhasedRollOut-test.js

diff --git a/src/PhasedRollOut.js b/src/PhasedRollOut.js
deleted file mode 100644
index b17ed37974..0000000000
--- a/src/PhasedRollOut.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import SdkConfig from './SdkConfig';
-import {hashCode} from './utils/FormattingUtils';
-
-export function phasedRollOutExpiredForUser(username, feature, now, rollOutConfig = SdkConfig.get().phasedRollOut) {
-    if (!rollOutConfig) {
-        console.log(`no phased rollout configuration, so enabling ${feature}`);
-        return true;
-    }
-    const featureConfig = rollOutConfig[feature];
-    if (!featureConfig) {
-        console.log(`${feature} doesn't have phased rollout configured, so enabling`);
-        return true;
-    }
-    if (!Number.isFinite(featureConfig.offset) || !Number.isFinite(featureConfig.period)) {
-        console.error(`phased rollout of ${feature} is misconfigured, ` +
-            `offset and/or period are not numbers, so disabling`, featureConfig);
-        return false;
-    }
-
-    const hash = hashCode(username);
-    //ms -> min, enable users at minute granularity
-    const bucketRatio = 1000 * 60;
-    const bucketCount = featureConfig.period / bucketRatio;
-    const userBucket = hash % bucketCount;
-    const userMs = userBucket * bucketRatio;
-    const enableAt = featureConfig.offset + userMs;
-    const result = now >= enableAt;
-    const bucketStr = `(bucket ${userBucket}/${bucketCount})`;
-    if (result) {
-        console.log(`${feature} enabled for ${username} ${bucketStr}`);
-    } else {
-        console.log(`${feature} will be enabled for ${username} in ${Math.ceil((enableAt - now)/1000)}s ${bucketStr}`);
-    }
-    return result;
-}
diff --git a/test/PhasedRollOut-test.js b/test/PhasedRollOut-test.js
deleted file mode 100644
index f02411d78d..0000000000
--- a/test/PhasedRollOut-test.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import {phasedRollOutExpiredForUser} from '../src/PhasedRollOut';
-
-const OFFSET = 6000000;
-// phasedRollOutExpiredForUser enables users in bucks of 1 minute
-const MS_IN_MINUTE = 60 * 1000;
-
-describe('PhasedRollOut', function() {
-    it('should return true if phased rollout is not configured', function() {
-        expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", 0, null)).toBeTruthy();
-    });
-
-    it('should return true if phased rollout feature is not configured', function() {
-        expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", 0, {
-            "feature_other": {offset: 0, period: 0},
-        })).toBeTruthy();
-    });
-
-    it('should return false if phased rollout for feature is misconfigured', function() {
-        expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", 0, {
-            "feature_test": {},
-        })).toBeFalsy();
-    });
-
-    it("should return false if phased rollout hasn't started yet", function() {
-        expect(phasedRollOutExpiredForUser("@user:hs", "feature_test", 5000000, {
-            "feature_test": {offset: OFFSET, period: MS_IN_MINUTE},
-        })).toBeFalsy();
-    });
-
-    it("should start to return true in bucket 2/10 for '@user:hs'", function() {
-        expect(phasedRollOutExpiredForUser("@user:hs", "feature_test",
-            OFFSET + (MS_IN_MINUTE * 2) - 1, {
-            "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10},
-        })).toBeFalsy();
-        expect(phasedRollOutExpiredForUser("@user:hs", "feature_test",
-            OFFSET + (MS_IN_MINUTE * 2), {
-            "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10},
-        })).toBeTruthy();
-    });
-
-    it("should start to return true in bucket 4/10 for 'alice@other-hs'", function() {
-        expect(phasedRollOutExpiredForUser("alice@other-hs", "feature_test",
-            OFFSET + (MS_IN_MINUTE * 4) - 1, {
-            "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10},
-        })).toBeFalsy();
-        expect(phasedRollOutExpiredForUser("alice@other-hs", "feature_test",
-            OFFSET + (MS_IN_MINUTE * 4), {
-            "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10},
-        })).toBeTruthy();
-    });
-
-    it("should return true after complete rollout period'", function() {
-        expect(phasedRollOutExpiredForUser("user:hs", "feature_test",
-            OFFSET + (MS_IN_MINUTE * 20), {
-            "feature_test": {offset: OFFSET, period: MS_IN_MINUTE * 10},
-        })).toBeTruthy();
-    });
-});

From d4df9e731d0053aa439f62d3e9dc0a67fa8128c4 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 00:10:47 +0000
Subject: [PATCH 088/389] i18n

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

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5bbbdf60b5..c3038fd9af 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1933,7 +1933,6 @@
     "Please provide a room address": "Please provide a room address",
     "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",

From 32cca0534c5ff7c7a86d854e6a00a4764652d20b Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 00:15:00 +0000
Subject: [PATCH 089/389] improve algo by skipping an O(n) operation

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

diff --git a/src/utils/objects.ts b/src/utils/objects.ts
index fe010df2b9..41e3f37d1b 100644
--- a/src/utils/objects.ts
+++ b/src/utils/objects.ts
@@ -89,9 +89,10 @@ export function objectHasDiff<O extends {}>(a: O, b: O): boolean {
     if (a === b) return false;
     const aKeys = Object.keys(a);
     const bKeys = Object.keys(b);
-    if (arrayHasDiff(aKeys, bKeys)) return true;
-
     const possibleChanges = arrayUnion(aKeys, bKeys);
+    // if the amalgamation of both sets of keys has the a different length to the inputs then there must be a change
+    if (possibleChanges.length !== aKeys.length) return true;
+
     return possibleChanges.some(k => a[k] !== b[k]);
 }
 

From 49f511bbab5a458f8be7e155f17ca317173d4a8e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 00:26:52 +0000
Subject: [PATCH 090/389] delint

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

diff --git a/src/utils/objects.ts b/src/utils/objects.ts
index 41e3f37d1b..166c31c4c3 100644
--- a/src/utils/objects.ts
+++ b/src/utils/objects.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { arrayDiff, arrayHasDiff, arrayMerge, arrayUnion } from "./arrays";
+import { arrayDiff, arrayMerge, arrayUnion } from "./arrays";
 
 type ObjectExcluding<O extends {}, P extends (keyof O)[]> = {[k in Exclude<keyof O, P[number]>]: O[k]};
 

From 02ae1e954b06b82122984e63100f980280e483c6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 11:40:34 +0000
Subject: [PATCH 091/389] clean up objectHasDiff and short circuit it quicker

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

diff --git a/src/utils/objects.ts b/src/utils/objects.ts
index 166c31c4c3..f240fe2f7d 100644
--- a/src/utils/objects.ts
+++ b/src/utils/objects.ts
@@ -89,10 +89,9 @@ export function objectHasDiff<O extends {}>(a: O, b: O): boolean {
     if (a === b) return false;
     const aKeys = Object.keys(a);
     const bKeys = Object.keys(b);
-    const possibleChanges = arrayUnion(aKeys, bKeys);
-    // if the amalgamation of both sets of keys has the a different length to the inputs then there must be a change
-    if (possibleChanges.length !== aKeys.length) return true;
+    if (aKeys.length !== bKeys.length) return true;
 
+    const possibleChanges = arrayUnion(aKeys, bKeys);
     return possibleChanges.some(k => a[k] !== b[k]);
 }
 

From 63d95706e9fe20b8ce5dbc30abada716efcfa79a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 12:58:23 +0000
Subject: [PATCH 092/389] Create setHasDiff helper and use it

The usage here is identical to how it'll work in Spaces
---
 .../filters/CommunityFilterCondition.ts       | 15 ++++----
 src/utils/sets.ts                             | 34 +++++++++++++++++++
 2 files changed, 41 insertions(+), 8 deletions(-)
 create mode 100644 src/utils/sets.ts

diff --git a/src/stores/room-list/filters/CommunityFilterCondition.ts b/src/stores/room-list/filters/CommunityFilterCondition.ts
index 924a85e86a..fbdfefb983 100644
--- a/src/stores/room-list/filters/CommunityFilterCondition.ts
+++ b/src/stores/room-list/filters/CommunityFilterCondition.ts
@@ -19,17 +19,17 @@ import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondi
 import { Group } from "matrix-js-sdk/src/models/group";
 import { EventEmitter } from "events";
 import GroupStore from "../../GroupStore";
-import { arrayHasDiff } from "../../../utils/arrays";
 import { IDestroyable } from "../../../utils/IDestroyable";
 import DMRoomMap from "../../../utils/DMRoomMap";
+import { setHasDiff } from "../../../utils/sets";
 
 /**
  * A filter condition for the room list which reveals rooms which
  * are a member of a given community.
  */
 export class CommunityFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
-    private roomIds: string[] = [];
-    private userIds: string[] = [];
+    private roomIds = new Set<string>();
+    private userIds = new Set<string>();
 
     constructor(private community: Group) {
         super();
@@ -45,19 +45,18 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon
     }
 
     public isVisible(room: Room): boolean {
-        return this.roomIds.includes(room.roomId) ||
-            this.userIds.includes(DMRoomMap.shared().getUserIdForRoomId(room.roomId));
+        return this.roomIds.has(room.roomId) || this.userIds.has(DMRoomMap.shared().getUserIdForRoomId(room.roomId));
     }
 
     private onStoreUpdate = async (): Promise<any> => {
         // We don't actually know if the room list changed for the community, so just check it again.
         const beforeRoomIds = this.roomIds;
-        this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
+        this.roomIds = new Set((await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId));
 
         const beforeUserIds = this.userIds;
-        this.userIds = (await GroupStore.getGroupMembers(this.community.groupId)).map(u => u.userId);
+        this.userIds = new Set((await GroupStore.getGroupMembers(this.community.groupId)).map(u => u.userId));
 
-        if (arrayHasDiff(beforeRoomIds, this.roomIds) || arrayHasDiff(beforeUserIds, this.userIds)) {
+        if (setHasDiff(beforeRoomIds, this.roomIds) || setHasDiff(beforeUserIds, this.userIds)) {
             this.emit(FILTER_CHANGED);
         }
     };
diff --git a/src/utils/sets.ts b/src/utils/sets.ts
new file mode 100644
index 0000000000..e5427b2e94
--- /dev/null
+++ b/src/utils/sets.ts
@@ -0,0 +1,34 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/**
+ * Determines if two sets are different through a shallow comparison.
+ * @param a The first set. Must be defined.
+ * @param b The second set. Must be defined.
+ * @returns True if they are different, false otherwise.
+ */
+export function setHasDiff<T>(a: Set<T>, b: Set<T>): boolean {
+    if (a.size === b.size) {
+        // When the lengths are equal, check to see if either set is missing an element from the other.
+        if (Array.from(b).some(i => !a.has(i))) return true;
+        if (Array.from(a).some(i => !b.has(i))) return true;
+
+        // if all the keys are common, say so
+        return false;
+    } else {
+        return true; // different lengths means they are naturally diverged
+    }
+}

From 413b37d3dcd21a63c279ab2c760df3704aaf00e4 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 13:06:45 +0000
Subject: [PATCH 093/389] Create and use Incompatible Settings Controller

---
 src/settings/Settings.ts                      |  3 ++
 .../controllers/IncompatibleController.ts     | 46 +++++++++++++++++++
 2 files changed, 49 insertions(+)
 create mode 100644 src/settings/controllers/IncompatibleController.ts

diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index ca5e2f1d04..ea58201322 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -38,6 +38,7 @@ import { UIFeature } from "./UIFeature";
 import { OrderedMultiController } from "./controllers/OrderedMultiController";
 import {Layout} from "./Layout";
 import ReducedMotionController from './controllers/ReducedMotionController';
+import IncompatibleController from "./controllers/IncompatibleController";
 
 // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
 const LEVELS_ROOM_SETTINGS = [
@@ -188,6 +189,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: _td("Show message previews for reactions in DMs"),
         supportedLevels: LEVELS_FEATURE,
         default: false,
+        // this option is a subset of `feature_roomlist_preview_reactions_all` so disable it when that one is enabled
+        controller: new IncompatibleController("feature_roomlist_preview_reactions_all"),
     },
     "feature_roomlist_preview_reactions_all": {
         isFeature: true,
diff --git a/src/settings/controllers/IncompatibleController.ts b/src/settings/controllers/IncompatibleController.ts
new file mode 100644
index 0000000000..c48ce0a60b
--- /dev/null
+++ b/src/settings/controllers/IncompatibleController.ts
@@ -0,0 +1,46 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import SettingController from "./SettingController";
+import { SettingLevel } from "../SettingLevel";
+import SettingsStore from "../SettingsStore";
+
+/**
+ * Enforces that a boolean setting cannot be enabled if the incompatible setting
+ * is also enabled, to prevent cascading undefined behaviour between conflicting
+ * labs flags.
+ */
+export default class IncompatibleController extends SettingController {
+    public constructor(private settingName: string, private forcedValue = false) {
+        super();
+    }
+
+    public getValueOverride(
+        level: SettingLevel,
+        roomId: string,
+        calculatedValue: any,
+        calculatedAtLevel: SettingLevel,
+    ): any {
+        if (this.incompatibleSettingEnabled) {
+            return this.forcedValue;
+        }
+        return null; // no override
+    }
+
+    public get incompatibleSettingEnabled(): boolean {
+        return SettingsStore.getValue(this.settingName);
+    }
+}

From f45510ed2593625cfdb561c7cfc631cea816154a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 13:08:30 +0000
Subject: [PATCH 094/389] Fix test assuming too much about the underlying calls

---
 test/components/views/rooms/MemberList-test.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js
index 24b6391c93..068d358dcd 100644
--- a/test/components/views/rooms/MemberList-test.js
+++ b/test/components/views/rooms/MemberList-test.js
@@ -88,7 +88,7 @@ describe('MemberList', () => {
         };
         memberListRoom.currentState = {
             members: {},
-            getStateEvents: () => [], // ignore 3pid invites
+            getStateEvents: (eventType, stateKey) => stateKey === undefined ? [] : null, // ignore 3pid invites
         };
         for (const member of [...adminUsers, ...moderatorUsers, ...defaultUsers]) {
             memberListRoom.currentState.members[member.userId] = member;

From a82357239477c6b2fba5d323b85bc0c21bd2b68f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 13:38:43 +0000
Subject: [PATCH 095/389] delint

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

diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index ea58201322..9ad0e8987e 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -36,7 +36,7 @@ import { isMac } from '../Keyboard';
 import UIFeatureController from "./controllers/UIFeatureController";
 import { UIFeature } from "./UIFeature";
 import { OrderedMultiController } from "./controllers/OrderedMultiController";
-import {Layout} from "./Layout";
+import { Layout } from "./Layout";
 import ReducedMotionController from './controllers/ReducedMotionController';
 import IncompatibleController from "./controllers/IncompatibleController";
 

From 148764aa8a8c23f282d7512956db515a7b64964f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 13:50:49 +0000
Subject: [PATCH 096/389] Create Labs flag for Spaces

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

diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index 9ad0e8987e..a8fa88179b 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -120,6 +120,13 @@ export interface ISetting {
 }
 
 export const SETTINGS: {[setting: string]: ISetting} = {
+    "feature_spaces": {
+        isFeature: true,
+        displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags"),
+        supportedLevels: LEVELS_FEATURE,
+        default: false,
+        controller: new ReloadOnChangeController(),
+    },
     "feature_latex_maths": {
         isFeature: true,
         displayName: _td("Render LaTeX maths in messages"),
@@ -134,6 +141,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         ),
         supportedLevels: LEVELS_FEATURE,
         default: false,
+        controller: new IncompatibleController("feature_spaces"),
     },
     "feature_new_spinner": {
         isFeature: true,
@@ -159,6 +167,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"),
         supportedLevels: LEVELS_FEATURE,
         default: false,
+        controller: new IncompatibleController("feature_spaces"),
     },
     "feature_state_counters": {
         isFeature: true,
@@ -733,6 +742,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
     [UIFeature.Communities]: {
         supportedLevels: LEVELS_UI_FEATURE,
         default: true,
+        controller: new IncompatibleController("feature_spaces"),
     },
     [UIFeature.AdvancedSettings]: {
         supportedLevels: LEVELS_UI_FEATURE,

From 6b3f05a3cdd58311875b0ad17f38c1cdc3331ef3 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 14:10:36 +0000
Subject: [PATCH 097/389] Switch RoomListStore to only including the filtered
 subset

Without this it'd include notification counts from Community B when Community A is selected and such.
---
 src/stores/room-list/RoomListStore.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts
index ea118a4c58..667d9de64d 100644
--- a/src/stores/room-list/RoomListStore.ts
+++ b/src/stores/room-list/RoomListStore.ts
@@ -58,8 +58,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
     private filterConditions: IFilterCondition[] = [];
     private tagWatcher = new TagWatcher(this);
     private updateFn = new MarkedExecution(() => {
-        for (const tagId of Object.keys(this.unfilteredLists)) {
-            RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.unfilteredLists[tagId]);
+        for (const tagId of Object.keys(this.orderedLists)) {
+            RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.orderedLists[tagId]);
         }
         this.emit(LISTS_UPDATE_EVENT);
     });

From 79daf615e4d8f99602c2c616baebf8d359a3e39e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 19 Feb 2021 14:20:57 +0000
Subject: [PATCH 098/389] First special treatment of space-rooms

---
 src/Avatar.ts                                  |  3 +++
 src/i18n/strings/en_EN.json                    |  1 +
 src/stores/BreadcrumbsStore.ts                 |  1 +
 .../room-list/filters/VisibilityProvider.ts    | 18 ++++++++++--------
 4 files changed, 15 insertions(+), 8 deletions(-)

diff --git a/src/Avatar.ts b/src/Avatar.ts
index 60bdfdcf75..e2557e21a8 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -165,6 +165,9 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
         return explicitRoomAvatar;
     }
 
+    // space rooms cannot be DMs so skip the rest
+    if (room.isSpaceRoom()) return null;
+
     let otherMember = null;
     const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
     if (otherUserId) {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index c3038fd9af..77451c1da8 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -777,6 +777,7 @@
     "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
     "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
     "Change notification settings": "Change notification settings",
+    "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags",
     "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",
diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts
index 24906f678c..393f4f27a1 100644
--- a/src/stores/BreadcrumbsStore.ts
+++ b/src/stores/BreadcrumbsStore.ts
@@ -122,6 +122,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
     }
 
     private async appendRoom(room: Room) {
+        if (room.isSpaceRoom() && SettingsStore.getValue("feature_spaces")) return; // hide space rooms
         let updated = false;
         const rooms = (this.state.rooms || []).slice(); // cheap clone
 
diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts
index af38141e5d..388bb061e3 100644
--- a/src/stores/room-list/filters/VisibilityProvider.ts
+++ b/src/stores/room-list/filters/VisibilityProvider.ts
@@ -18,6 +18,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
 import CallHandler from "../../../CallHandler";
 import { RoomListCustomisations } from "../../../customisations/RoomList";
 import VoipUserMapper from "../../../VoipUserMapper";
+import SettingsStore from "../../../settings/SettingsStore";
 
 export class VisibilityProvider {
     private static internalInstance: VisibilityProvider;
@@ -37,22 +38,23 @@ export class VisibilityProvider {
     }
 
     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
-
         if (
             CallHandler.sharedInstance().getSupportsVirtualRooms() &&
             VoipUserMapper.sharedInstance().isVirtualRoom(room)
         ) {
-            isVisible = false;
-            forced = true;
+            return false;
+        }
+
+        // hide space rooms as they'll be shown in the SpacePanel
+        if (room.isSpaceRoom() && SettingsStore.getValue("feature_spaces")) {
+            return false;
         }
 
         const isVisibleFn = RoomListCustomisations.isRoomVisible;
-        if (!forced && isVisibleFn) {
-            isVisible = isVisibleFn(room);
+        if (isVisibleFn) {
+            return isVisibleFn(room);
         }
 
-        return isVisible;
+        return true; // default
     }
 }

From 540a8114ab8de75b926932a8c1554d8a14c7d237 Mon Sep 17 00:00:00 2001
From: libexus <Asterixeins324@gmail.com>
Date: Fri, 19 Feb 2021 17:59:25 +0000
Subject: [PATCH 099/389] Translated using Weblate (German)

Currently translated at 99.8% (2760 of 2764 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 | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index bc37bb7bb8..e4d0a6ae70 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -131,7 +131,7 @@
     "Jun": "Jun",
     "Jul": "Juli",
     "Aug": "Aug",
-    "Sep": "Sep",
+    "Sep": "Sept",
     "Oct": "Okt",
     "Nov": "Nov",
     "Dec": "Dez",
@@ -1235,7 +1235,7 @@
     "Your Matrix account on %(serverName)s": "Dein Matrixkonto auf %(serverName)s",
     "Name or Matrix ID": "Name oder Matrix ID",
     "Your %(brand)s is misconfigured": "Dein %(brand)s ist falsch konfiguriert",
-    "You cannot modify widgets in this room.": "Du kannst in diesem Raum keine Widgets verändern.",
+    "You cannot modify widgets in this room.": "Du darfst in diesem Raum keine Widgets verändern.",
     "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Ob du die \"Breadcrumbs\"-Funktion nutzt oder nicht (Avatare oberhalb der Raumliste)",
     "The server does not support the room version specified.": "Der Server unterstützt die angegebene Raumversion nicht.",
     "<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Achtung</b>: Ein Raum-Upgrade wird <i>die Mitglieder des Raumes nicht automatisch auf die neue Version migrieren.</i> Wir werden in der alten Raumversion einen Link zum neuen Raum posten - Raum-Mitglieder müssen dann auf diesen Link klicken um dem neuen Raum beizutreten.",
@@ -1303,7 +1303,7 @@
     "Identity Server URL must be HTTPS": "Die Identity Server-URL muss HTTPS sein",
     "Could not connect to Identity Server": "Verbindung zum Identity Server konnte nicht hergestellt werden",
     "Checking server": "Server wird überprüft",
-    "Identity server has no terms of service": "Für den Identity Server gelten keine Nutzungsbedingungen",
+    "Identity server has no terms of service": "Der Identitätsserver hat keine Nutzungsbedingungen",
     "Disconnect": "Trennen",
     "Identity Server": "Identitätsserver",
     "Use an identity server": "Benutze einen Identitätsserver",
@@ -1416,7 +1416,7 @@
     "View rules": "Regeln betrachten",
     "You are currently subscribed to:": "Du abonnierst momentan:",
     "⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.",
-    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem Berührung der primäre Eingabemechanismus ist",
+    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem Touch das primäre Eingabegerät ist",
     "Whether you're using %(brand)s as an installed Progressive Web App": "Ob du %(brand)s als installierte progressive Web-App verwendest",
     "Your user agent": "Dein User-Agent",
     "If you cancel now, you won't complete verifying the other user.": "Wenn Sie jetzt abbrechen, werden Sie die Verifizierung des anderen Nutzers nicht beenden können.",
@@ -1427,7 +1427,7 @@
     "Verifies a user, session, and pubkey tuple": "Verifiziert einen Benutzer, eine Sitzung und Pubkey-Tupel",
     "Unknown (user, session) pair:": "Unbekanntes (Nutzer-, Sitzungs-) Paar:",
     "Session already verified!": "Sitzung bereits verifiziert!",
-    "WARNING: Session already verified, but keys do NOT MATCH!": "ACHTUNG: Sitzung bereits verifiziert, aber die Schlüssel passen NICHT ZUSAMMEN!",
+    "WARNING: Session already verified, but keys do NOT MATCH!": "WARNUNG: Die Sitzung wurde bereits verifiziert, aber die Schlüssel passen NICHT ZUSAMMEN!",
     "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ACHTUNG: SCHLÜSSEL-VERIFIZIERUNG FEHLGESCHLAGEN! Der Signierschlüssel für %(userId)s und Sitzung %(deviceId)s ist \"%(fprint)s\", was nicht mit dem bereitgestellten Schlüssel \"%(fingerprint)s\" übereinstimmt. Das könnte bedeuten, dass deine Kommunikation abgehört wird!",
     "Never send encrypted messages to unverified sessions from this session": "Sende niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen",
     "Never send encrypted messages to unverified sessions in this room from this session": "Sende niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen in diesem Raum",
@@ -1645,7 +1645,7 @@
     "Command failed": "Befehl fehlgeschlagen",
     "Could not find user in room": "Der Benutzer konnte im Raum nicht gefunden werden",
     "Click the button below to confirm adding this email address.": "Klicke unten auf die Schaltfläche, um die hinzugefügte E-Mail-Adresse zu bestätigen.",
-    "Confirm adding phone number": "Bestätige hinzugefügte Telefonnummer",
+    "Confirm adding phone number": "Hinzugefügte Telefonnummer bestätigen",
     "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschluss-Regel für Server von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
     "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s erneuert eine Ausschluss-Regel von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
     "Not Trusted": "Nicht vertraut",

From 1086d8249974f196cad5113c4a5a307d34e93597 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Fri, 19 Feb 2021 14:48:38 +0000
Subject: [PATCH 100/389] Translated using Weblate (German)

Currently translated at 99.8% (2760 of 2764 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 | 140 +++++++++++++++++++-----------------
 1 file changed, 75 insertions(+), 65 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index e4d0a6ae70..fdd427b81f 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -12,10 +12,10 @@
     "Name": "Name",
     "Session ID": "Sitzungs-ID",
     "Displays action": "Zeigt Aktionen an",
-    "Bans user with given id": "Verbannt den Benutzer mit der angegebenen ID",
-    "Deops user with given id": "Setzt das Berechtigungslevel beim Benutzer mit der angegebenen ID zurück",
-    "Invites user with given id to current room": "Lädt den Benutzer mit der angegebenen ID in den aktuellen Raum ein",
-    "Kicks user with given id": "Benutzer mit der angegebenen ID kicken",
+    "Bans user with given id": "Verbannt den/die Benutzer:in mit der angegebenen ID",
+    "Deops user with given id": "Setzt das Berechtigungslevel des/der Benutzer:in mit der angegebenen ID zurück",
+    "Invites user with given id to current room": "Lädt den/die Benutzer:in mit der angegebenen ID in den aktuellen Raum ein",
+    "Kicks user with given id": "Benutzer:in mit der angegebenen ID kicken",
     "Changes your display nickname": "Ändert deinen angezeigten Nicknamen",
     "Change Password": "Passwort ändern",
     "Searches DuckDuckGo for results": "Verwendet DuckDuckGo für Suchergebnisse",
@@ -89,7 +89,7 @@
     "unknown error code": "Unbekannter Fehlercode",
     "Upload avatar": "Profilbild hochladen",
     "Upload file": "Datei hochladen",
-    "Users": "Benutzer",
+    "Users": "Nutzer:innen",
     "Verification Pending": "Verifizierung ausstehend",
     "Video call": "Videoanruf",
     "Voice call": "Sprachanruf",
@@ -181,7 +181,7 @@
     "%(senderName)s unbanned %(targetName)s.": "%(senderName)s hat die Verbannung von %(targetName)s aufgehoben.",
     "Usage": "Verwendung",
     "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s hat die Einladung für %(targetName)s zurückgezogen.",
-    "You need to be able to invite users to do that.": "Du musst die Berechtigung haben, Benutzer einzuladen, um diese Aktion ausführen zu können.",
+    "You need to be able to invite users to do that.": "Du brauchst die Berechtigung Benutzer:innen einzuladen haben, um diese Aktion ausführen zu können.",
     "You need to be logged in.": "Du musst angemeldet sein.",
     "There are no visible files in this room": "Es gibt keine sichtbaren Dateien in diesem Raum",
     "Connectivity to the server has been lost.": "Verbindung zum Server wurde unterbrochen.",
@@ -285,7 +285,7 @@
     "Error decrypting video": "Video-Entschlüsselung fehlgeschlagen",
     "Import room keys": "Raum-Schlüssel importieren",
     "File to import": "Zu importierende Datei",
-    "Failed to invite the following users to the %(roomName)s room:": "Folgende Benutzer konnten nicht in den Raum \"%(roomName)s\" eingeladen werden:",
+    "Failed to invite the following users to the %(roomName)s room:": "Folgende Benutzer:innen konnten nicht in den Raum \"%(roomName)s\" eingeladen werden:",
     "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.": "Bist du sicher, dass du dieses Ereignis entfernen (löschen) möchtest? Wenn du die Änderung eines Raum-Namens oder eines Raum-Themas löscht, kann dies dazu führen, dass die ursprüngliche Änderung rückgängig gemacht wird.",
     "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Dieser Prozess erlaubt es dir, die Schlüssel für die in verschlüsselten Räumen empfangenen Nachrichten in eine lokale Datei zu exportieren. In Zukunft wird es möglich sein, diese Datei in einen anderen Matrix-Client zu importieren, sodass dieser Client diese Nachrichten ebenfalls entschlüsseln kann.",
     "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.": "Mit der exportierten Datei kann jeder, der diese Datei lesen kann, jede verschlüsselte Nachricht entschlüsseln, die für dich lesbar ist. Du solltest die Datei also unbedingt sicher verwahren. Um den Vorgang sicherer zu gestalten, solltest du unten eine Passphrase eingeben, die dazu verwendet wird, die exportierten Daten zu verschlüsseln. Anschließend wird es nur möglich sein, die Daten zu importieren, wenn dieselbe Passphrase verwendet wird.",
@@ -416,31 +416,31 @@
     "You are now ignoring %(userId)s": "%(userId)s wird jetzt ignoriert",
     "You are no longer ignoring %(userId)s": "%(userId)s wird nicht mehr ignoriert",
     "Leave": "Verlassen",
-    "Failed to invite the following users to %(groupId)s:": "Die folgenden Benutzer konnten nicht in die Gruppe %(groupId)s eingeladen werden:",
+    "Failed to invite the following users to %(groupId)s:": "Die folgenden Benutzer:innen konnten nicht in die Gruppe %(groupId)s eingeladen werden:",
     "Leave %(groupName)s?": "%(groupName)s verlassen?",
     "Add a Room": "Raum hinzufügen",
-    "Add a User": "Benutzer hinzufügen",
+    "Add a User": "Benutzer:in hinzufügen",
     "You have entered an invalid address.": "Du hast eine ungültige Adresse eingegeben.",
     "Matrix ID": "Matrix-ID",
     "Unignore": "Nicht mehr ignorieren",
-    "Unignored user": "Benutzer nicht mehr ignoriert",
-    "Ignored user": "Benutzer ignoriert",
+    "Unignored user": "Benutzer:in nicht mehr ignoriert",
+    "Ignored user": "Benutzer:in ignoriert",
     "Stops ignoring a user, showing their messages going forward": "Beendet das Ignorieren eines Benutzers, nachfolgende Nachrichten werden wieder angezeigt",
-    "Ignores a user, hiding their messages from you": "Ignoriert einen Benutzer und verbirgt dessen Nachrichten",
+    "Ignores a user, hiding their messages from you": "Ignoriert eine:n Benutzer:in und verbirgt dessen/deren Nachrichten",
     "Banned by %(displayName)s": "Verbannt von %(displayName)s",
     "Description": "Beschreibung",
     "Unable to accept invite": "Einladung kann nicht angenommen werden",
-    "Failed to invite users to %(groupId)s": "Benutzer konnten nicht in %(groupId)s eingeladen werden",
+    "Failed to invite users to %(groupId)s": "Benutzer:innen konnten nicht in %(groupId)s eingeladen werden",
     "Unable to reject invite": "Einladung konnte nicht abgelehnt werden",
     "Who would you like to add to this summary?": "Wen möchtest zu dieser Übersicht hinzufügen?",
     "Add to summary": "Zur Übersicht hinzufügen",
-    "Failed to add the following users to the summary of %(groupId)s:": "Die folgenden Benutzer konnten nicht zur Übersicht von %(groupId)s hinzugefügt werden:",
+    "Failed to add the following users to the summary of %(groupId)s:": "Die folgenden Benutzer:innen konnten nicht zur Übersicht von %(groupId)s hinzugefügt werden:",
     "Which rooms would you like to add to this summary?": "Welche Räume möchtest du zu dieser Übersicht hinzufügen?",
     "Failed to add the following rooms to the summary of %(groupId)s:": "Die folgenden Räume konnten nicht zur Übersicht von %(groupId)s hinzugefügt werden:",
     "Failed to remove the room from the summary of %(groupId)s": "Der Raum konnte nicht aus der Übersicht von %(groupId)s entfernt werden",
     "The room '%(roomName)s' could not be removed from the summary.": "Der Raum '%(roomName)s' konnte nicht aus der Übersicht entfernt werden.",
-    "Failed to remove a user from the summary of %(groupId)s": "Benutzer konnte nicht aus der Übersicht von %(groupId)s entfernt werden",
-    "The user '%(displayName)s' could not be removed from the summary.": "Der Benutzer '%(displayName)s' konnte nicht aus der Übersicht entfernt werden.",
+    "Failed to remove a user from the summary of %(groupId)s": "Benutzer:in konnte nicht aus der Übersicht von %(groupId)s entfernt werden",
+    "The user '%(displayName)s' could not be removed from the summary.": "Der Benutzer:in '%(displayName)s' konnte nicht aus der Übersicht entfernt werden.",
     "Unknown": "Unbekannt",
     "Failed to add the following rooms to %(groupId)s:": "Die folgenden Räume konnten nicht zu %(groupId)s hinzugefügt werden:",
     "Matrix Room ID": "Matrix-Raum-ID",
@@ -469,7 +469,7 @@
     "Which rooms would you like to add to this community?": "Welche Räume sollen zu dieser Community hinzugefügt werden?",
     "Add rooms to the community": "Räume zur Community hinzufügen",
     "Add to community": "Zur Community hinzufügen",
-    "Failed to invite users to community": "Benutzer konnten nicht in die Community eingeladen werden",
+    "Failed to invite users to community": "Benutzer:innen konnten nicht in die Community eingeladen werden",
     "Communities": "Communities",
     "Invalid community ID": "Ungültige Community-ID",
     "'%(groupId)s' is not a valid community ID": "'%(groupId)s' ist keine gültige Community-ID",
@@ -485,20 +485,20 @@
     "Community ID": "Community-ID",
     "example": "Beispiel",
     "Add rooms to the community summary": "Fügt Räume zur Community-Übersicht hinzu",
-    "Add users to the community summary": "Füge Benutzer zur Community-Übersicht hinzu",
+    "Add users to the community summary": "Füge Benutzer:innen zur Community-Übersicht hinzu",
     "Failed to update community": "Aktualisieren der Community fehlgeschlagen",
     "Leave Community": "Community verlassen",
     "Add rooms to this community": "Räume zu dieser Community hinzufügen",
     "%(inviter)s has invited you to join this community": "%(inviter)s hat dich in diese Community eingeladen",
     "You are a member of this community": "Du bist Mitglied dieser Community",
-    "You are an administrator of this community": "Du bist ein Administrator dieser Community",
+    "You are an administrator of this community": "Du bist ein:e Administrator:in dieser Community",
     "Community %(groupId)s not found": "Community '%(groupId)s' nicht gefunden",
     "Failed to load %(groupId)s": "'%(groupId)s' konnte nicht geladen werden",
     "Error whilst fetching joined communities": "Fehler beim Laden beigetretener Communities",
     "Create a new community": "Neue Community erstellen",
     "Your Communities": "Deine Communities",
     "You're not currently a member of any communities.": "Du gehörst aktuell keiner Community an.",
-    "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Erstelle eine Community, um Benutzer und Räume miteinander zu verbinden! Erstelle zusätzlich eine eigene Homepage, um deinen individuellen Bereich im Matrix-Universum zu gestalten.",
+    "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Erstelle eine Community, um Benutzer:innen und Räume miteinander zu verbinden! Erstelle zusätzlich eine eigene Homepage, um deinen individuellen Bereich im Matrix-Universum zu gestalten.",
     "Something went wrong whilst creating your community": "Beim Erstellen deiner Community ist ein Fehler aufgetreten",
     "And %(count)s more...|other": "Und %(count)s weitere...",
     "Delete Widget": "Widget löschen",
@@ -550,17 +550,17 @@
     "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)shaben das Profilbild geändert",
     "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)shat das Profilbild %(count)s-mal geändert",
     "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)shat das Profilbild geändert",
-    "Disinvite this user?": "Einladung für diesen Benutzer zurückziehen?",
-    "Kick this user?": "Diesen Benutzer kicken?",
-    "Unban this user?": "Verbannung für diesen Benutzer aufheben?",
-    "Ban this user?": "Diesen Benutzer verbannen?",
+    "Disinvite this user?": "Einladung für diese:n Benutzer:in zurückziehen?",
+    "Kick this user?": "Diese:n Benutzer:in kicken?",
+    "Unban this user?": "Verbannung für diese:n Benutzer:in aufheben?",
+    "Ban this user?": "Diese:n Benutzer:in verbannen?",
     "Members only (since the point in time of selecting this option)": "Nur Mitglieder (ab dem Zeitpunkt, an dem diese Option ausgewählt wird)",
     "Members only (since they were invited)": "Nur Mitglieder (ab dem Zeitpunkt, an dem sie eingeladen wurden)",
     "Members only (since they joined)": "Nur Mitglieder (ab dem Zeitpunkt, an dem sie beigetreten sind)",
     "An email has been sent to %(emailAddress)s": "Eine E-Mail wurde an %(emailAddress)s gesendet",
     "A text message has been sent to %(msisdn)s": "Eine Textnachricht wurde an %(msisdn)s gesendet",
-    "Disinvite this user from community?": "Community-Einladung für diesen Benutzer zurückziehen?",
-    "Remove this user from community?": "Diesen Benutzer aus der Community entfernen?",
+    "Disinvite this user from community?": "Community-Einladung für diese:n Benutzer:in zurückziehen?",
+    "Remove this user from community?": "Diese:n Benutzer:in aus der Community entfernen?",
     "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)shaben ihre Einladungen %(count)s-mal abgelehnt",
     "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)shat die Einladung %(count)s-mal abgelehnt",
     "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)shat die Einladung abgelehnt",
@@ -629,7 +629,7 @@
     "Whether or not you're using the Richtext mode of the Rich Text Editor": "Ob du den Richtext-Modus des Editors benutzt oder nicht",
     "Your homeserver's URL": "Deine Homeserver-URL",
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
-    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der letze Nutzer mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.",
+    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der/die letze Nutzer:in mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.",
     "Community IDs cannot be empty.": "Community-IDs können nicht leer sein.",
     "Learn more about how we use analytics.": "Lerne mehr darüber, wie wir die Analysedaten nutzen.",
     "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Wenn diese Seite identifizierbare Informationen wie Raum-, Nutzer- oder Gruppen-ID enthält, werden diese Daten entfernt bevor sie an den Server gesendet werden.",
@@ -802,14 +802,14 @@
     "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Dies wird deinen Account permanent unbenutzbar machen. Du wirst nicht in der Lage sein, dich anzumelden und keiner wird dieselbe Benutzer-ID erneut registrieren können. Alle Räume, in denen der Account ist, werden verlassen und deine Account-Daten werden vom Identitätsserver gelöscht. <b>Diese Aktion ist unumkehrbar.</b>",
     "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Standardmäßig werden <b>die von dir gesendeten Nachrichten beim Deaktiveren nicht gelöscht</b>. Wenn du dies von uns möchtest, aktivere das Auswalfeld unten.",
     "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Die Sichtbarkeit der Nachrichten in Matrix ist vergleichbar mit E-Mails: Wenn wir deine Nachrichten vergessen heißt das, dass diese nicht mit neuen oder nicht registrierten Nutzern teilen werden, aber registrierte Nutzer, die bereits zugriff haben, werden Zugriff auf ihre Kopie behalten.",
-    "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Bitte vergesst alle Nachrichten, die ich gesendet habe, wenn mein Account deaktiviert wird. (<b>Warnung:</b> Zukünftige Nutzer werden eine unvollständige Konversation sehen)",
+    "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Bitte vergesst alle Nachrichten, die ich gesendet habe, wenn mein Account deaktiviert wird. (<b>Warnung:</b> Zukünftige Nutzer:innen werden eine unvollständige Konversation sehen)",
     "To continue, please enter your password:": "Um fortzufahren, bitte Passwort eingeben:",
     "Can't leave Server Notices room": "Du kannst den Raum für Server-Notizen nicht verlassen",
     "This room is used for important messages from the Homeserver, so you cannot leave it.": "Du kannst diesen Raum nicht verlassen, da dieser Raum für wichtige Nachrichten vom Heimserver verwendet wird.",
     "Terms and Conditions": "Geschäftsbedingungen",
     "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Um den %(homeserverDomain)s -Heimserver weiter zu verwenden, musst du die Geschäftsbedingungen sichten und ihnen zustimmen.",
     "Review terms and conditions": "Geschäftsbedingungen anzeigen",
-    "Share Link to User": "Link zum Benutzer teilen",
+    "Share Link to User": "Link zum/r Benutzer:in teilen",
     "Share room": "Raum teilen",
     "Share Room": "Raum teilen",
     "Link to most recent message": "Link zur aktuellsten Nachricht",
@@ -851,7 +851,7 @@
     "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Deine Nachricht wurde nicht gesendet, weil dieser Heimserver ein Ressourcen-Limit erreicht hat. Bitte <a>kontaktiere deinen Systemadministrator</a> um diesen Dienst weiter zu nutzen.",
     "Please <a>contact your service administrator</a> to continue using this service.": "Bitte <a>kontaktiere deinen Systemadministrator</a> um diesen Dienst weiter zu nutzen.",
     "Sorry, your homeserver is too old to participate in this room.": "Sorry, dein Homeserver ist zu alt, um an diesem Raum teilzunehmen.",
-    "Please contact your homeserver administrator.": "Bitte setze dich mit dem Administrator deines Homeservers in Verbindung.",
+    "Please contact your homeserver administrator.": "Bitte setze dich mit der Administration deines Homeservers in Verbindung.",
     "Legal": "Rechtliches",
     "This room has been replaced and is no longer active.": "Dieser Raum wurde ersetzt und ist nicht länger aktiv.",
     "The conversation continues here.": "Die Konversation wird hier fortgesetzt.",
@@ -865,7 +865,7 @@
     "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s setzte die Hauptadresse zu diesem Raum auf %(address)s.",
     "%(senderName)s removed the main address for this room.": "%(senderName)s entfernte die Hauptadresse von diesem Raum.",
     "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Bevor du Log-Dateien übermittelst, musst du ein <a>GitHub-Issue erstellen</a> um dein Problem zu beschreiben.",
-    "%(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 benutzt nun 3-5-mal weniger Arbeitsspeicher, indem Informationen über andere Nutzer erst bei Bedarf geladen werden. Bitte warte, während die Daten erneut mit dem Server abgeglichen werden!",
+    "%(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 benutzt nun 3 - 5-mal weniger Arbeitsspeicher, indem Informationen über andere Nutzer:innen erst bei Bedarf geladen werden. Bitte warte, während die Daten erneut mit dem Server abgeglichen werden!",
     "Updating %(brand)s": "Aktualisiere %(brand)s",
     "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.": "Du hast zuvor %(brand)s auf %(host)s ohne das verzögerte Laden von Mitgliedern genutzt. In dieser Version war das verzögerte Laden deaktiviert. Da die lokal zwischengespeicherten Daten zwischen diesen Einstellungen nicht kompatibel sind, muss %(brand)s dein Konto neu synchronisieren.",
     "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Wenn %(brand)s mit der alten Version in einem anderen Tab geöffnet ist, schließe dies bitte, da das parallele Nutzen von %(brand)s auf demselben Host mit aktivierten und deaktivierten verzögertem Laden, Probleme verursachen wird.",
@@ -873,7 +873,7 @@
     "Clear cache and resync": "Zwischenspeicher löschen und erneut synchronisieren",
     "Please review and accept the policies of this homeserver:": "Bitte sieh dir alle Bedingungen dieses Heimservers an und akzeptiere sie:",
     "Add some now": "Jemanden jetzt hinzufügen",
-    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Du bist ein Administrator dieser Community. Du wirst nicht erneut hinzutreten können, wenn du nicht von einem anderen Administrator eingeladen wirst.",
+    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Du bist ein:e Administrator:in dieser Community. Du wirst nicht erneut hinzutreten können, wenn du nicht von einem/r anderen Administrator:in eingeladen wirst.",
     "Open Devtools": "Öffne Entwickler-Werkzeuge",
     "Show developer tools": "Zeige Entwickler-Werkzeuge",
     "Unable to load! Check your network connectivity and try again.": "Konnte nicht geladen werden! Überprüfe die Netzwerkverbindung und versuche es erneut.",
@@ -923,9 +923,9 @@
     "Names and surnames by themselves are easy to guess": "Namen und Familiennamen alleine sind einfach zu erraten",
     "Common names and surnames are easy to guess": "Häufige Namen und Familiennamen sind einfach zu erraten",
     "You do not have permission to invite people to this room.": "Du hast keine Berechtigung um Personen in diesen Raum einzuladen.",
-    "User %(user_id)s does not exist": "Benutzer %(user_id)s existiert nicht",
+    "User %(user_id)s does not exist": "Benutzer:in %(user_id)s existiert nicht",
     "Unknown server error": "Unbekannter Server-Fehler",
-    "Failed to invite users to the room:": "Konnte Benutzer nicht in den Raum einladen:",
+    "Failed to invite users to the room:": "Konnte Benutzer:innen nicht in den Raum einladen:",
     "Short keyboard patterns are easy to guess": "Kurze Tastaturmuster sind einfach zu erraten",
     "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Zeige eine Erinnerung um die Sichere Nachrichten-Wiederherstellung in verschlüsselten Räumen zu aktivieren",
     "Messages containing @room": "Nachrichten die \"@room\" enthalten",
@@ -962,9 +962,9 @@
     "Go to Settings": "Gehe zu Einstellungen",
     "Sign in with single sign-on": "Melde dich mit „Single Sign-On“ an",
     "Unrecognised address": "Nicht erkannte Adresse",
-    "User %(user_id)s may or may not exist": "Existenz der Benutzer %(user_id)s unsicher",
+    "User %(user_id)s may or may not exist": "Existenz des/der Benutzers/in %(user_id)s unsicher",
     "Prompt before sending invites to potentially invalid matrix IDs": "Nachfragen bevor Einladungen zu möglichen ungültigen Matrix IDs gesendet werden",
-    "The following users may not exist": "Eventuell existieren folgende Benutzer nicht",
+    "The following users may not exist": "Eventuell existieren folgende Benutzer:innen nicht",
     "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Profile für die unteren Matrix IDs wurden nicht gefunden - willst Du sie trotzdem einladen?",
     "Invite anyway and never warn me again": "Trotzdem einladen und mich nicht mehr warnen",
     "Invite anyway": "Trotzdem einladen",
@@ -989,8 +989,8 @@
     "Messages containing my username": "Nachrichten, die meinen Benutzernamen enthalten",
     "The other party cancelled the verification.": "Die Gegenstelle hat die Überprüfung abgebrochen.",
     "Verified!": "Verifiziert!",
-    "You've successfully verified this user.": "Du hast diesen Benutzer erfolgreich verifiziert.",
-    "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sichere Nachrichten mit diesem Benutzer sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden.",
+    "You've successfully verified this user.": "Du hast diese:n Benutzer:in erfolgreich verifiziert.",
+    "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sichere Nachrichten mit diesem/r Benutzer:in sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden.",
     "Got It": "Verstanden",
     "Verify this user by confirming the following number appears on their screen.": "Verifiziere diese Nutzer!n, indem du bestätigst, dass die folgende Nummer auf dessen Bildschirm erscheint.",
     "Yes": "Ja",
@@ -1194,8 +1194,8 @@
     "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s aktivierte Abzeichen der Gruppen %(groups)s für diesen Raum.",
     "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s deaktivierte Abzeichen der Gruppen %(groups)s in diesem Raum.",
     "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s aktivierte Abzeichen von %(newGroups)s und deaktivierte die Abzeichen von %(oldGroups)s in diesem Raum.",
-    "User %(userId)s is already in the room": "Nutzer %(userId)s ist bereits im Raum",
-    "The user must be unbanned before they can be invited.": "Nutzer müssen entbannt werden, bevor sie eingeladen werden können.",
+    "User %(userId)s is already in the room": "Nutzer:in %(userId)s ist bereits im Raum",
+    "The user must be unbanned before they can be invited.": "Die Verbannung des/der Nutzer:in muss aufgehoben werden, bevor er/sie eingeladen werden kann.",
     "Show read receipts sent by other users": "Zeige Lesebestätigungen anderer Benutzer",
     "Scissors": "Scheren",
     "<a>Upgrade</a> to your own domain": "<a>Upgrade</a> zu deiner eigenen Domain",
@@ -1209,10 +1209,10 @@
     "Modify widgets": "Ändere Widgets",
     "Default role": "Standard Rolle",
     "Send messages": "Sende Nachrichten",
-    "Invite users": "Benutzer einladen",
+    "Invite users": "Benutzer:innen einladen",
     "Change settings": "Ändere Einstellungen",
-    "Kick users": "Benutzer kicken",
-    "Ban users": "Benutzer verbannen",
+    "Kick users": "Benutzer:innen kicken",
+    "Ban users": "Benutzer:innen verbannen",
     "Remove messages": "Nachrichten löschen",
     "Notify everyone": "Jeden Benachrichtigen",
     "Send %(eventType)s events": "Sende %(eventType)s-Ereignisse",
@@ -1243,7 +1243,7 @@
     "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Momentan ist es nicht möglich mit einer Datei zu antworten. Möchtest Du die Datei hochladen ohne zu antworten?",
     "The file '%(fileName)s' failed to upload.": "Die Datei \"%(fileName)s\" konnte nicht hochgeladen werden.",
     "Changes your avatar in this current room only": "Ändert deinen Avatar für diesen Raum",
-    "Unbans user with given ID": "Entbannt den Benutzer mit der angegebenen ID",
+    "Unbans user with given ID": "Hebt die Verbannung des/der Benutzer:in mit der angegebenen ID auf",
     "Sends the given message coloured as a rainbow": "Sendet die Nachricht in Regenbogenfarben",
     "Adds a custom widget by URL to the room": "Fügt ein Benutzer-Widget über eine URL zum Raum hinzu",
     "Please supply a https:// or http:// widget URL": "Bitte gib eine https:// oder http:// Widget-URL an",
@@ -1317,7 +1317,7 @@
     "Enter a new identity server": "Gib einen neuen Identitätsserver ein",
     "Clear personal data": "Persönliche Daten löschen",
     "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.": "Wenn du die Verbindung zu deinem Identitätsserver trennst, heißt das, dass du nicht mehr von anderen Benutzern gefunden werden und auch andere nicht mehr per E-Mail oder Telefonnummer einladen kannst.",
-    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Bitte frage den Administrator deines Heimservers (<code>%(homeserverDomain)s</code>) darum, einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren.",
+    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Bitte frage die Administration deines Heimservers (<code>%(homeserverDomain)s</code>) darum, einen TURN-Server einzurichten, damit Anrufe zuverlässig funktionieren.",
     "Disconnect from the identity server <idserver />?": "Verbindung zum Identitätsserver <idserver /> trennen?",
     "Add Email Address": "E-Mail-Adresse hinzufügen",
     "Add Phone Number": "Telefonnummer hinzufügen",
@@ -1390,7 +1390,7 @@
     "Disconnect from the identity server <current /> and connect to <new /> instead?": "Vom Identitätsserver <current /> trennen, und stattdessen eine Verbindung zu <new /> aufbauen?",
     "The identity server you have chosen does not have any terms of service.": "Der von dir gewählte Identitätsserver hat keine Nutzungsbedingungen.",
     "Disconnect identity server": "Verbindung zum Identitätsserver trennen",
-    "contact the administrators of identity server <idserver />": "Administrator des Identitätsservers <idserver /> kontaktieren",
+    "contact the administrators of identity server <idserver />": "Administration des Identitätsservers <idserver /> kontaktieren",
     "wait and try again later": "warte und versuche es später erneut",
     "Disconnect anyway": "Verbindung trotzdem trennen",
     "You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Du <b>teilst deine persönlichen Daten</b> immer noch auf dem Identitätsserver <idserver />.",
@@ -1415,7 +1415,7 @@
     "Unsubscribe": "Deabonnieren",
     "View rules": "Regeln betrachten",
     "You are currently subscribed to:": "Du abonnierst momentan:",
-    "⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.",
+    "⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer:innen gedacht.",
     "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem Touch das primäre Eingabegerät ist",
     "Whether you're using %(brand)s as an installed Progressive Web App": "Ob du %(brand)s als installierte progressive Web-App verwendest",
     "Your user agent": "Dein User-Agent",
@@ -1454,7 +1454,7 @@
     "Go Back": "Zurückgehen",
     "Notification Autocomplete": "Benachrichtigung Autovervollständigen",
     "If disabled, messages from encrypted rooms won't appear in search results.": "Wenn deaktiviert, werden Nachrichten von verschlüsselten Räumen nicht in den Ergebnissen auftauchen.",
-    "This user has not verified all of their sessions.": "Dieser Benutzer hat nicht alle seine Sitzungen verifiziert.",
+    "This user has not verified all of their sessions.": "Diese:r Benutzer:in hat nicht alle seine/ihre Sitzungen verifiziert.",
     "You have verified this user. This user has verified all of their sessions.": "Du hast diese/n Nutzer!n verifiziert. Er/Sie hat alle seine/ihre Sitzungen verifiziert.",
     "Your key share request has been sent - please check your other sessions for key share requests.": "Deine Schlüsselanfrage wurde gesendet - sieh in deinen anderen Sitzungen nach der Schlüsselanfrage.",
     "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast klicke hier, um die Schlüssel erneut anzufordern.",
@@ -1490,12 +1490,12 @@
     "Use bots, bridges, widgets and sticker packs": "Benutze Bots, Bridges, Widgets und Sticker-Packs",
     "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Wenn du dein Passwort änderst, werden alle Ende-zu-Ende-Verschlüsselungsschlüssel für alle deine Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist. Richte ein Schlüssel-Backup ein oder exportiere deine Raumschlüssel aus einer anderen Sitzung, bevor du dein Passwort zurücksetzst.",
     "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Du wurdest von allen Sitzungen abgemeldet und erhälst keine Push-Benachrichtigungen mehr. Um die Benachrichtigungen wieder zu aktivieren, melde dich auf jedem Gerät erneut an.",
-    "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Aktualisiere diese Sitzung, damit sie andere Sitzungen verifizieren kann, indem sie dir Zugang zu verschlüsselten Nachrichten gewährt und sie für andere Benutzer als vertrauenswürdig markiert.",
+    "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Aktualisiere diese Sitzung, damit sie andere Sitzungen verifizieren kann, indem sie dir Zugang zu verschlüsselten Nachrichten gewährt und sie für andere Benutzer:innen als vertrauenswürdig markiert.",
     "Sign out and remove encryption keys?": "Abmelden und Verschlüsselungsschlüssel entfernen?",
     "Sign in to your Matrix account on <underlinedServerName />": "Melde dich bei deinem Matrix-Konto auf <underlinedServerName /> an",
     "Enter your password to sign in and regain access to your account.": "Gib dein Passwort ein, um dich anzumelden und wieder Zugang zu deinem Konto zu erhalten.",
     "Sign in and regain access to your account.": "Melden dich an und erhalte wieder Zugang zu deinem Konto.",
-    "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Du kannst dich nicht bei deinem Konto anmelden. Bitte kontaktiere deinen Homeserver-Administrator für weitere Informationen.",
+    "You cannot sign in to your account. Please contact your homeserver admin for more information.": "Du kannst dich nicht bei deinem Konto anmelden. Bitte kontaktiere deine Homeserver-Administration für weitere Informationen.",
     "Sign In or Create Account": "Anmelden oder Account erstellen",
     "Use your account or create a new one to continue.": "Benutze deinen Account oder erstellen einen neuen, um fortzufahren.",
     "Create Account": "Account erstellen",
@@ -1643,7 +1643,7 @@
     "%(name)s is requesting verification": "%(name)s fordert eine Verifizierung an",
     "Failed to set topic": "Das Festlegen des Themas ist fehlgeschlagen",
     "Command failed": "Befehl fehlgeschlagen",
-    "Could not find user in room": "Der Benutzer konnte im Raum nicht gefunden werden",
+    "Could not find user in room": "Benutzer:in konnte nicht im Raum gefunden werden",
     "Click the button below to confirm adding this email address.": "Klicke unten auf die Schaltfläche, um die hinzugefügte E-Mail-Adresse zu bestätigen.",
     "Confirm adding phone number": "Hinzugefügte Telefonnummer bestätigen",
     "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschluss-Regel für Server von %(oldGlob)s nach %(newGlob)s wegen %(reason)s",
@@ -1728,7 +1728,7 @@
     "Verify all your sessions to ensure your account & messages are safe": "Verifiziere alle deine Sitzungen, um dein Konto und deine Nachrichten zu schützen",
     "Verify your other session using one of the options below.": "Verifiziere deine andere Sitzung mit einer der folgenden Optionen.",
     "You signed in to a new session without verifying it:": "Du hast dich in einer neuen Sitzung angemeldet ohne sie zu verifizieren:",
-    "Other users may not trust it": "Andere Benutzer vertrauen ihr vielleicht nicht",
+    "Other users may not trust it": "Andere Benutzer:innen vertrauen ihr vielleicht nicht",
     "Upgrade": "Hochstufen",
     "Verify the new login accessing your account: %(name)s": "Verifiziere die neue Anmeldung an deinem Konto: %(name)s",
     "From %(deviceName)s (%(deviceId)s)": "Von %(deviceName)s (%(deviceId)s)",
@@ -1841,7 +1841,7 @@
     "No other published addresses yet, add one below": "Keine anderen öffentlichen Adressen vorhanden, füge unten eine hinzu",
     "New published address (e.g. #alias:server)": "Neue öffentliche Adresse (z.B. #alias:server)",
     "Local Addresses": "Lokale Adressen",
-    "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Erstelle Adressen für diesen Raum, damit andere Benutzer den Raum auf deinem Heimserver (%(localDomain)s) finden können",
+    "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Erstelle Adressen für diesen Raum, damit andere Nutzer:innen den Raum auf deinem Heimserver (%(localDomain)s) finden können",
     "Waiting for you to accept on your other session…": "Warte auf die Bestätigung in deiner anderen Sitzung…",
     "Waiting for %(displayName)s to accept…": "Warte auf die Annahme von %(displayName)s …",
     "Accepting…": "Annehmen…",
@@ -1861,8 +1861,8 @@
     "This client does not support end-to-end encryption.": "Diese Anwendung unterstützt keine Ende-zu-Ende-Verschlüsselung.",
     "Verify by scanning": "Verifizierung durch QR-Code-Scannen",
     "If you can't scan the code above, verify by comparing unique emoji.": "Wenn du den obigen Code nicht scannen kannst, verifiziere stattdessen durch den Emoji-Vergleich.",
-    "Verify all users in a room to ensure it's secure.": "Verifiziere alle Benutzer in einem Raum um die vollständige Sicherheit zu gewährleisten.",
-    "In encrypted rooms, verify all users to ensure it’s secure.": "Verifiziere alle Benutzer in verschlüsselten Räumen um die vollständige Sicherheit zu gewährleisten.",
+    "Verify all users in a room to ensure it's secure.": "Verifiziere alle Benutzer:innen in einem Raum um die vollständige Sicherheit zu gewährleisten.",
+    "In encrypted rooms, verify all users to ensure it’s secure.": "Verifiziere alle Benutzer:innen in verschlüsselten Räumen um die vollständige Sicherheit zu gewährleisten.",
     "You've successfully verified %(deviceName)s (%(deviceId)s)!": "Du hast %(deviceName)s (%(deviceId)s) erfolgreich verifiziert!",
     "Verified": "Verifiziert",
     "Start verification again from the notification.": "Starte die Verifikation aus der Benachrichtigung erneut.",
@@ -2052,8 +2052,8 @@
     "Continue with previous account": "Mit vorherigen Konto fortfahren",
     "<a>Log in</a> to your new account.": "Mit deinem neuen Konto <a>anmelden</a>.",
     "You can now close this window or <a>log in</a> to your new account.": "Du kannst dieses Fenster jetzt schließen oder dich mit deinem neuen Konto <a>anmelden</a>.",
-    "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer sehen sie als vertrauenswürdig an.",
-    "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.",
+    "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten, und andere Benutzer:innen sehen sie als vertrauenswürdig an.",
+    "Your new session is now verified. Other users will see it as trusted.": "Deine neue Sitzung ist nun verifiziert. Andere Benutzer:innen 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>.": "Um ein Matrix-bezogenes Sicherheitsproblem zu melden, lies bitte die Matrix.org <a>Sicherheitsrichtlinien</a>.",
@@ -2074,7 +2074,7 @@
     "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Verifiziere dieses Gerät und es wird es als vertrauenswürdig markiert. Benutzer, die sich bei dir verifiziert haben, werden diesem Gerät auch vertrauen.",
     "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Dein %(brand)s erlaubt dir nicht, eine Integrationsverwaltung zu verwenden, um dies zu tun. Bitte kontaktiere einen Administrator.",
     "We couldn't create your DM. Please check the users you want to invite and try again.": "Wir konnten deine Direktnachricht nicht erstellen. Bitte überprüfe den Benutzer, den du einladen möchtest, und versuche es erneut.",
-    "We couldn't invite those users. Please check the users you want to invite and try again.": "Wir konnten diese Benutzer nicht einladen. Bitte überprüfe sie und versuche es erneut.",
+    "We couldn't invite those users. Please check the users you want to invite and try again.": "Wir konnten diese Benutzer:innen nicht einladen. Bitte überprüfe sie und versuche es erneut.",
     "Start a conversation with someone using their name, username (like <userId/>) or email address.": "Starte eine Unterhaltung mit jemandem indem du seinen Namen, Benutzernamen (z.B. <userId/>) oder E-Mail-Adresse eingibst.",
     "Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.": "Lade jemanden mit seinem Namen, Benutzernamen (z.B. <userId/>) oder E-Mail-Adresse ein oder <a>teile diesen Raum</a>.",
     "Upload completed": "Hochladen abgeschlossen",
@@ -2096,7 +2096,7 @@
     "Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.": "Die Sicherung konnte nicht mit dem angegebenen Wiederherstellungsschlüssel entschlüsselt werden: Bitte überprüfe ob du den richtigen Wiederherstellungsschlüssel eingegeben hast.",
     "Backup could not be decrypted with this recovery passphrase: please verify that you entered the correct recovery passphrase.": "Die Sicherung konnte mit diesem Wiederherstellungsschlüssel nicht entschlüsselt werden: Bitte überprüfe ob du die richtige Wiederherstellungspassphrase eingegeben hast.",
     "Nice, strong password!": "Super, ein starkes Passwort!",
-    "Other users can invite you to rooms using your contact details": "Andere Benutzer können dich mit deinen Kontaktdaten in Räume einladen",
+    "Other users can invite you to rooms using your contact details": "Andere Benutzer:innen können dich mit deinen Kontaktdaten in Räume einladen",
     "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Lege eine E-Mail für die Kontowiederherstellung fest. Verwende optional E-Mail oder Telefon, um von Anderen gefunden zu werden.",
     "Explore Public Rooms": "Öffentliche Räume erkunden",
     "If you've joined lots of rooms, this might take a while": "Du bist einer Menge Räumen beigetreten, das kann eine Weile dauern",
@@ -2174,7 +2174,7 @@
     "Toggle this dialog": "Diesen Dialog ein-/ausblenden",
     "Move autocomplete selection up/down": "Auto-Vervollständigung nach oben/unten verschieben",
     "Opens chat with the given user": "Öffnet einen Chat mit diesem Benutzer",
-    "Sends a message to the given user": "Sendet diesem Benutzer eine Nachricht",
+    "Sends a message to the given user": "Sendet diesem/r Benutzer:in eine Nachricht",
     "Waiting for your other session to verify…": "Warte auf die Verifikation deiner anderen Sitzungen…",
     "You've successfully verified your device!": "Du hast dein Gerät erfolgreich verifiziert!",
     "QR Code": "QR-Code",
@@ -2225,7 +2225,7 @@
     "Address (optional)": "Adresse (optional)",
     "delete the address.": "lösche die Adresse.",
     "Use a different passphrase?": "Eine andere Passphrase verwenden?",
-    "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Dein Server-Administrator hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.",
+    "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Deine Server-Administration hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.",
     "People": "Personen",
     "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Beim Entfernen dieser Adresse ist ein Fehler aufgetreten. Vielleicht existiert diese nicht mehr oder es kam zu einem temporären Fehler.",
     "Set a room address to easily share your room with other people.": "Vergebe eine Raum-Adresse, um diesen Raum auf einfache Weise mit anderen Personen teilen zu können.",
@@ -2641,7 +2641,7 @@
     "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/>).",
+    "Start a conversation with someone using their name, email address or username (like <userId/>).": "Beginne eine Konversation mit jemanden unter Benutzung des Namens, der Email-Adresse oder der Matrix-ID (wie <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:",
@@ -2979,7 +2979,7 @@
     "Sends the given message with snowfall": "Sendet die gewählte Nachricht mit Schneeflocken",
     "Transfer": "Übertragen",
     "Failed to transfer call": "Anruf-Übertragung fehlgeschlagen",
-    "A call can only be transferred to a single user.": "Ein Anruf kann nur auf einen einzelnen Nutzer übertragen werden.",
+    "A call can only be transferred to a single user.": "Ein Anruf kann nur auf eine:n einzelne:n Nutzer:in übertragen werden.",
     "Set up with a Security Key": "Mit einem Sicherheitsschlüssel einrichten",
     "Use Security Key": "Sicherheitsschlüssel benutzen",
     "Use Security Key or Phrase": "Sicherheitsschlüssel oder -phrase benutzen",
@@ -3003,7 +3003,7 @@
     "Workspace: <networkLink/>": "Arbeitsraum: <networkLink/>",
     "Dial pad": "Wähltastatur",
     "There was an error looking up the phone number": "Beim Suchen der Telefonnummer ist ein Fehler aufgetreten",
-    "Change which room, message, or user you're viewing": "Ändere welchen Raum, Nachricht oder Nutzer du siehst",
+    "Change which room, message, or user you're viewing": "Ändere welchen Raum, Nachricht oder Nutzer:in du siehst",
     "Unable to look up phone number": "Telefonnummer kann nicht gesucht werden",
     "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "In dieser Sitzung wurde festgestellt, dass deine Sicherheitsphrase und dein Schlüssel für sichere Nachrichten entfernt wurden.",
     "A new Security Phrase and key for Secure Messages have been detected.": "Eine neue Sicherheitsphrase und ein neuer Schlüssel für sichere Nachrichten wurden erkannt.",
@@ -3049,5 +3049,15 @@
     "Recently visited rooms": "Kürzlich besuchte Räume",
     "Show line numbers in code blocks": "Zeilennummern in Code-Blöcken anzeigen",
     "Expand code blocks by default": "Code-Blöcke standardmäßig erweitern",
-    "Try again": "Erneut versuchen"
+    "Try again": "Erneut versuchen",
+    "Upgrade to pro": "Hochstufen zu Pro",
+    "Minimize dialog": "Dialog minimieren",
+    "Maximize dialog": "Dialog maximieren",
+    "%(hostSignupBrand)s Setup": "%(hostSignupBrand)s-Einrichtung",
+    "You should know": "Du solltest wissen",
+    "Privacy Policy": "Datenschutz-Richtlinie",
+    "Cookie Policy": "Cookie-Richtlinie",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Erfahren mehr in unserer <privacyPolicyLink />, <termsOfServiceLink /> und <cookiePolicyLink />.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Verbindung zum Homeserver fehlgeschlagen. Bitte schließe diesen Dialog and versuche es erneut.",
+    "Abort": "Abbrechen"
 }

From bce88efa4f1a69a45ad83c61aac9110ec04a899a Mon Sep 17 00:00:00 2001
From: Germain <germain@grabyo.com>
Date: Sun, 21 Feb 2021 16:15:32 +0000
Subject: [PATCH 101/389] update isUserOnDarkTheme to return correct theme when
 use_system_theme is true

Signed-off-by: Germain <germain@grabyo.com>
---
 src/components/structures/UserMenu.tsx | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 7f96e2d142..5ed6a00d74 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -103,11 +103,15 @@ export default class UserMenu extends React.Component<IProps, IState> {
     };
 
     private isUserOnDarkTheme(): boolean {
-        const theme = SettingsStore.getValue("theme");
-        if (theme.startsWith("custom-")) {
-            return getCustomTheme(theme.substring("custom-".length)).is_dark;
+        if (SettingsStore.getValue("use_system_theme")) {
+            return window.matchMedia("(prefers-color-scheme: dark)").matches;
+        } else {
+            const theme = SettingsStore.getValue("theme");
+            if (theme.startsWith("custom-")) {
+                return getCustomTheme(theme.substring("custom-".length)).is_dark;
+            }
+            return theme === "dark";
         }
-        return theme === "dark";
     }
 
     private onProfileUpdate = async () => {

From d62ac4c80ae971bf654cbe7afd4203e2fec571e4 Mon Sep 17 00:00:00 2001
From: libexus <Asterixeins324@gmail.com>
Date: Sat, 20 Feb 2021 21:30:08 +0000
Subject: [PATCH 102/389] Translated using Weblate (German)

Currently translated at 99.3% (2747 of 2764 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, 47 insertions(+), 47 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index fdd427b81f..d92aa27e0a 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -160,7 +160,7 @@
     "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s hat den zukünftigen Chatverlauf für alle Raum-Mitglieder sichtbar gemacht (ab dem Zeitpunkt, an dem sie eingeladen wurden).",
     "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s hat den zukünftigen Chatverlauf für alle Raum-Mitglieder sichtbar gemacht (ab dem Zeitpunkt, an dem sie beigetreten sind).",
     "%(senderName)s made future room history visible to all room members.": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für: Alle Raum-Mitglieder.",
-    "%(senderName)s made future room history visible to anyone.": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für: Alle.",
+    "%(senderName)s made future room history visible to anyone.": "%(senderName)s hat den zukünftigen Chatverlauf für alle sichtbar gemacht.",
     "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s hat den zukünftigen Chatverlauf für Unbekannte sichtbar gemacht (%(visibility)s).",
     "Missing room_id in request": "Fehlende room_id in Anfrage",
     "Missing user_id in request": "Fehlende user_id in Anfrage",
@@ -357,7 +357,7 @@
     "Custom": "Erweitert",
     "Decline": "Ablehnen",
     "Drop File Here": "Lasse Datei hier los",
-    "Failed to upload profile picture!": "Hochladen des Profilbild's fehlgeschlagen!",
+    "Failed to upload profile picture!": "Hochladen des Profilbilds fehlgeschlagen!",
     "Incoming call from %(name)s": "Eingehender Anruf von %(name)s",
     "Incoming video call from %(name)s": "Eingehender Videoanruf von %(name)s",
     "Incoming voice call from %(name)s": "Eingehender Sprachanruf von %(name)s",
@@ -759,7 +759,7 @@
     "Unhide Preview": "Vorschau wieder anzeigen",
     "Unable to join network": "Es ist nicht möglich, dem Netzwerk beizutreten",
     "Sorry, your browser is <b>not</b> able to run %(brand)s.": "Es tut uns leid, aber dein Browser kann %(brand)s <b>nicht</b> ausführen.",
-    "Messages in group chats": "Nachrichten in Gruppen-Chats",
+    "Messages in group chats": "Nachrichten in Gruppenchats",
     "Yesterday": "Gestern",
     "Error encountered (%(errorDetail)s).": "Es ist ein Fehler aufgetreten (%(errorDetail)s).",
     "Low Priority": "Niedrige Priorität",
@@ -875,7 +875,7 @@
     "Add some now": "Jemanden jetzt hinzufügen",
     "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Du bist ein:e Administrator:in dieser Community. Du wirst nicht erneut hinzutreten können, wenn du nicht von einem/r anderen Administrator:in eingeladen wirst.",
     "Open Devtools": "Öffne Entwickler-Werkzeuge",
-    "Show developer tools": "Zeige Entwickler-Werkzeuge",
+    "Show developer tools": "Zeige Entwicklerwerkzeuge",
     "Unable to load! Check your network connectivity and try again.": "Konnte nicht geladen werden! Überprüfe die Netzwerkverbindung und versuche es erneut.",
     "Delete Backup": "Sicherung löschen",
     "Backup version: ": "Sicherungsversion: ",
@@ -909,7 +909,7 @@
     "All-uppercase is almost as easy to guess as all-lowercase": "Alles groß zu geschrieben ist fast genauso schnell zu raten, wie alles klein zu schreiben",
     "Reversed words aren't much harder to guess": "Umgedrehte Worte sind nicht schwerer zu erraten",
     "Predictable substitutions like '@' instead of 'a' don't help very much": "Vorhersagbare Ersetzungen wie '@' anstelle von 'a' helfen nicht viel",
-    "Add another word or two. Uncommon words are better.": "Füge ein weiteres wort hinzu - oder mehr. Ungewöhnliche Worte sind besser.",
+    "Add another word or two. Uncommon words are better.": "Füge ein weiteres Wort hinzu - oder mehr. Ungewöhnliche Worte sind besser.",
     "Repeats like \"aaa\" are easy to guess": "Wiederholungen wie \"aaa\" sind einfach zu erraten",
     "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Wiederholungen wie \"abcabcabc\" sind nur leicht schwerer zu raten als \"abc\"",
     "Sequences like abc or 6543 are easy to guess": "Sequenzen wie \"abc\" oder \"6543\" sind leicht zu raten",
@@ -924,11 +924,11 @@
     "Common names and surnames are easy to guess": "Häufige Namen und Familiennamen sind einfach zu erraten",
     "You do not have permission to invite people to this room.": "Du hast keine Berechtigung um Personen in diesen Raum einzuladen.",
     "User %(user_id)s does not exist": "Benutzer:in %(user_id)s existiert nicht",
-    "Unknown server error": "Unbekannter Server-Fehler",
+    "Unknown server error": "Unbekannter Serverfehler",
     "Failed to invite users to the room:": "Konnte Benutzer:innen nicht in den Raum einladen:",
     "Short keyboard patterns are easy to guess": "Kurze Tastaturmuster sind einfach zu erraten",
     "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Zeige eine Erinnerung um die Sichere Nachrichten-Wiederherstellung in verschlüsselten Räumen zu aktivieren",
-    "Messages containing @room": "Nachrichten die \"@room\" enthalten",
+    "Messages containing @room": "Nachrichten, die \"@room\" enthalten",
     "Encrypted messages in one-to-one chats": "Verschlüsselte Nachrichten in 1:1 Chats",
     "Encrypted messages in group chats": "Verschlüsselte Nachrichten in Gruppenchats",
     "Use a longer keyboard pattern with more turns": "Nutze ein längeres Tastaturmuster mit mehr Abwechslung",
@@ -963,7 +963,7 @@
     "Sign in with single sign-on": "Melde dich mit „Single Sign-On“ an",
     "Unrecognised address": "Nicht erkannte Adresse",
     "User %(user_id)s may or may not exist": "Existenz des/der Benutzers/in %(user_id)s unsicher",
-    "Prompt before sending invites to potentially invalid matrix IDs": "Nachfragen bevor Einladungen zu möglichen ungültigen Matrix IDs gesendet werden",
+    "Prompt before sending invites to potentially invalid matrix IDs": "Nachfragen, bevor Einladungen zu möglichen ungültigen Matrix-IDs gesendet werden",
     "The following users may not exist": "Eventuell existieren folgende Benutzer:innen nicht",
     "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Profile für die unteren Matrix IDs wurden nicht gefunden - willst Du sie trotzdem einladen?",
     "Invite anyway and never warn me again": "Trotzdem einladen und mich nicht mehr warnen",
@@ -987,12 +987,12 @@
     "Enable big emoji in chat": "Aktiviere große Emoji im Chat",
     "Enable Community Filter Panel": "Community-Filter-Panel aktivieren",
     "Messages containing my username": "Nachrichten, die meinen Benutzernamen enthalten",
-    "The other party cancelled the verification.": "Die Gegenstelle hat die Überprüfung abgebrochen.",
+    "The other party cancelled the verification.": "Die Gegenstelle hat die Verifizierung abgebrochen.",
     "Verified!": "Verifiziert!",
     "You've successfully verified this user.": "Du hast diese:n Benutzer:in erfolgreich verifiziert.",
     "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sichere Nachrichten mit diesem/r Benutzer:in sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden.",
     "Got It": "Verstanden",
-    "Verify this user by confirming the following number appears on their screen.": "Verifiziere diese Nutzer!n, indem du bestätigst, dass die folgende Nummer auf dessen Bildschirm erscheint.",
+    "Verify this user by confirming the following number appears on their screen.": "Verifiziere diese Nutzer:in, indem du bestätigst, dass die folgende Nummer auf dessen Bildschirm erscheint.",
     "Yes": "Ja",
     "No": "Nein",
     "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Wir haben dir eine E-Mail geschickt, um deine Adresse zu überprüfen. Bitte folge den Anweisungen dort und klicke dann auf die Schaltfläche unten.",
@@ -1031,13 +1031,13 @@
     "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Die Datei '%(fileName)s' überschreitet die maximale Größe für Uploads auf diesem Heimserver",
     "This room has no topic.": "Dieser Raum hat kein Thema.",
     "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s machte den Raum für jeden, der den Link kennt öffentlich.",
-    "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s hat den Raum auf eingeladene User beschränkt.",
+    "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s hat den Raum auf eingeladene Benutzer beschränkt.",
     "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s änderte die Zutrittsregel auf '%(rule)s'",
     "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s erlaubte Gäste diesem Raum beizutreten.",
-    "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s hat Gästen verboten diesem Raum beizutreten.",
+    "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s hat Gästen verboten, diesem Raum beizutreten.",
     "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s änderte den Gastzugriff auf '%(rule)s'",
     "Group & filter rooms by custom tags (refresh to apply changes)": "Gruppiere & filtere Räume nach eigenen Tags (neu laden um Änderungen zu übernehmen)",
-    "Unable to find a supported verification method.": "Konnte kein unterstützte Verifikationsmethode finden.",
+    "Unable to find a supported verification method.": "Konnte keine unterstützte Verifikationsmethode finden.",
     "Dog": "Hund",
     "Cat": "Katze",
     "Lion": "Löwe",
@@ -1111,7 +1111,7 @@
     "Ignored users": "Ignorierte Benutzer",
     "Key backup": "Schlüsselsicherung",
     "Gets or sets the room topic": "Frage das Thema des Raums ab oder setze es",
-    "Verify this user by confirming the following emoji appear on their screen.": "Verifiziere diese Nutzer!n, indem du bestätigst, dass folgendes Emoji auf dessen Bildschirm erscheint.",
+    "Verify this user by confirming the following emoji appear on their screen.": "Verifiziere diese Nutzer:in, indem du bestätigst, dass folgendes Emoji auf dessen Bildschirm erscheint.",
     "Missing media permissions, click the button below to request.": "Fehlende Medienberechtigungen. Drücke auf den Knopf unten, um sie anzufordern.",
     "Request media permissions": "Medienberechtigungen anfordern",
     "Main address": "Primäre Adresse",
@@ -1328,7 +1328,7 @@
     "Find a room…": "Einen Raum suchen…",
     "Find a room… (e.g. %(exampleRoom)s)": "Einen Raum suchen… (z.B. %(exampleRoom)s)",
     "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Wenn du den gesuchten Raum nicht finden kannst, frage nach einer Einladung für den Raum oder <a>Erstelle einen neuen Raum</a>.",
-    "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ kannst du versuchen, den öffentlichen Server unter <code>turn.matrix.org</code> zu verwenden. Allerdings wird dieser nicht so zuverlässig sein, und du teilst deine IP-Adresse mit diesem Server. Du kannst dies auch in den Einstellungen konfigurieren.",
+    "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ kannst du versuchen, den öffentlichen Server unter <code>turn.matrix.org</code> zu verwenden. Allerdings wird dieser nicht so zuverlässig sein und du teilst deine IP-Adresse mit diesem Server. Du kannst dies auch in den Einstellungen konfigurieren.",
     "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Diese Handlung erfordert es, auf den Standard-Identitätsserver <server /> zuzugreifen, um eine E-Mail Adresse oder Telefonnummer zu validieren, aber der Server hat keine Nutzungsbedingungen.",
     "Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du dem/r Besitzer:in des Servers vertraust.",
     "Trust": "Vertrauen",
@@ -1339,7 +1339,7 @@
     "Try out new ways to ignore people (experimental)": "Versuche neue Möglichkeiten, um Menschen zu ignorieren (experimentell)",
     "Send read receipts for messages (requires compatible homeserver to disable)": "Lesebestätigungen für Nachrichten senden (Deaktivieren erfordert einen kompatiblen Heimserver)",
     "My Ban List": "Meine Bannliste",
-    "This is your list of users/servers you have blocked - don't leave the room!": "Dies ist die Liste von Benutzer*innen/Servern, die du blockiert hast - verlasse den Raum nicht!",
+    "This is your list of users/servers you have blocked - don't leave the room!": "Dies ist die Liste von Benutzer:innen und Servern, die du blockiert hast - verlasse diesen Raum nicht!",
     "Accept <policyLink /> to continue:": "Akzeptiere <policyLink />, um fortzufahren:",
     "Change identity server": "Wechsle den Identitätsserver",
     "You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "Du solltest deine <b>persönlichen Daten</b> vom Identitätsserver <idserver /> entfernen, bevor du die Verbindung trennst. Leider ist der Identitätsserver <idserver /> derzeit offline oder kann nicht erreicht werden.",
@@ -1568,7 +1568,7 @@
     "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s hat die alternativen Adressen %(addresses)s für diesen Raum entfernt.",
     "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s hat die alternative Adresse %(addresses)s für diesen Raum entfernt.",
     "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s hat die alternative Adresse für diesen Raum geändert.",
-    "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s hat die Haupt- und Alternativadresse für diesen Raum geändert.",
+    "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s hat die Haupt- und Alternativadressen für diesen Raum geändert.",
     "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Nutzer!nnen, die %(glob)s entsprechen",
     "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Räume, die %(glob)s entsprechen",
     "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Server, die %(glob)s entsprechen",
@@ -2202,7 +2202,7 @@
     "I want to help": "Ich möchte helfen",
     "Your homeserver has exceeded its user limit.": "Dein Heimserver hat das Benutzerlimit erreicht.",
     "Your homeserver has exceeded one of its resource limits.": "Dein Heimserver hat eine seiner Ressourcengrenzen erreicht.",
-    "Contact your <a>server admin</a>.": "Kontaktiere deinen <a>Heimserver Administrator</a>.",
+    "Contact your <a>server admin</a>.": "Kontaktiere deinen <a>Heimserver-Administrator</a>.",
     "Ok": "Ok",
     "Set password": "Setze Passwort",
     "To return to your account in future you need to set a password": "Um dein Konto zukünftig wieder verwenden zu können, setze ein Passwort",
@@ -2557,8 +2557,8 @@
     "Places the call in the current room on hold": "Den aktuellen Anruf halten",
     "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",
+    "Send stickers into your active room": "Sticker 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",
@@ -2574,7 +2574,7 @@
     "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",
+    "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",
@@ -2583,33 +2583,33 @@
     "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",
+    "Send messages as you in your active room": "Eine Nachricht als du in deinen aktiven Raum senden",
+    "See messages posted to this room": "In diesen Raum gesendete Nachrichten anzeigen",
+    "See messages posted to your active room": "In deinen aktiven Raum gesendete Nachrichten anzeigen",
+    "Send text messages as you in this room": "Textnachrichten als du in diesen Raum senden",
+    "Send text messages as you in your active room": "Textnachrichten als du in deinen aktiven Raum senden",
+    "See text messages posted to this room": "In diesen Raum gesendete Textnachrichten anzeigen",
+    "See text messages posted to your active room": "In deinen aktiven Raum gesendete Textnachrichten anzeigen",
+    "Send emotes as you in this room": "Emojis als du in diesen Raum senden",
+    "Send emotes as you in your active room": "Emojis als du in deinen 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",
+    "See emotes posted to your active room": "In deinen aktiven Raum gesendete Emojis anzeigen",
+    "See videos posted to your active room": "In deinen aktiven Raum gesendete Videos anzeigen",
+    "See videos posted to this room": "In diesen Raum gesendete Videos anzeigen",
+    "Send images as you in this room": "Bilder als du in diesen 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",
+    "See images posted to this room": "In diesen Raum gesendete Bilder anzeigen",
+    "See images posted to your active room": "In deinen aktiven Raum gesendete Bilder anzeigen",
+    "Send videos as you in this room": "Videos als du in diesen Raum senden",
+    "Send videos as you in your active room": "Videos als du in deinen aktiven Raum senden",
+    "Send general files as you in this room": "Allgemeine Dateien als du in diesen Raum senden",
+    "Send general files as you in your active room": "Allgemeine Dateien als du in deinen aktiven Raum senden",
+    "See general files posted to your active room": "Allgemeine in deinen aktiven Raum gesendete Dateien anzeigen",
+    "See general files posted to this room": "Allgemeine in diesen Raum gesendete Dateien anzeigen",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Sende <b>%(msgtype)s</b> Nachrichten als du in diesen Raum",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "Sende <b>%(msgtype)s</b> Nachrichten als du in deinen aktiven Raum",
+    "See <b>%(msgtype)s</b> messages posted to this room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in diesen Raum gesendet worden sind",
+    "See <b>%(msgtype)s</b> messages posted to your active room": "Zeige <b>%(msgtype)s</b> Nachrichten, welche in deinen aktiven Raum gesendet worden sind",
     "Don't miss a reply": "Verpasse keine Antwort",
     "Enable desktop notifications": "Aktiviere Desktopbenachrichtigungen",
     "Update %(brand)s": "Aktualisiere %(brand)s",
@@ -2963,7 +2963,7 @@
     "sends fireworks": "sendet Feuerwerk",
     "Sends the given message with fireworks": "Sendet die gewählte Nachricht mit Feuerwerk",
     "sends confetti": "sendet Konfetti",
-    "Sends the given message with confetti": "Sendet die gewählte Nachricht ohne Konfetti",
+    "Sends the given message with confetti": "Sendet die gewählte Nachricht mit Konfetti",
     "Show chat effects": "Chat-Effekte anzeigen",
     "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Stellt ┬──┬ ノ( ゜-゜ノ) einer Klartextnachricht voran",
     "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Stellt (╯°□°)╯︵ ┻━┻ einer Klartextnachricht voran",

From c76cc33ebfc4e501e0322c3d70ca88770c70bbc4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Mon, 22 Feb 2021 13:23:39 +0100
Subject: [PATCH 103/389] Don't show copy button if there is no <code>
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/messages/TextualBody.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 6b201f1b8e..632e2e4afe 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -161,6 +161,9 @@ export default class TextualBody extends React.Component {
     }
 
     _addCodeCopyButton(div) {
+        const copyCode = div.getElementsByTagName("code")[0];
+        // If there isn't any code element don't show the copy button
+        if (!copyCode) return;
         const button = document.createElement("span");
         button.className = "mx_EventTile_button mx_EventTile_copyButton ";
 
@@ -170,7 +173,6 @@ export default class TextualBody extends React.Component {
         if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
 
         button.onclick = async () => {
-            const copyCode = button.parentNode.getElementsByTagName("code")[0];
             const successful = await copyPlaintext(copyCode.textContent);
 
             const buttonRect = button.getBoundingClientRect();

From 87a2454556dedd332fbd44c2fd8894a6dc5379b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Mon, 22 Feb 2021 13:41:20 +0100
Subject: [PATCH 104/389] Revert "Don't show copy button if there is no <code>"

This reverts commit c76cc33ebfc4e501e0322c3d70ca88770c70bbc4.
---
 src/components/views/messages/TextualBody.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 632e2e4afe..6b201f1b8e 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -161,9 +161,6 @@ export default class TextualBody extends React.Component {
     }
 
     _addCodeCopyButton(div) {
-        const copyCode = div.getElementsByTagName("code")[0];
-        // If there isn't any code element don't show the copy button
-        if (!copyCode) return;
         const button = document.createElement("span");
         button.className = "mx_EventTile_button mx_EventTile_copyButton ";
 
@@ -173,6 +170,7 @@ export default class TextualBody extends React.Component {
         if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
 
         button.onclick = async () => {
+            const copyCode = button.parentNode.getElementsByTagName("code")[0];
             const successful = await copyPlaintext(copyCode.textContent);
 
             const buttonRect = button.getBoundingClientRect();

From 6149567c62e219ec733a1de7cdcdfc3ed791bd9a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Mon, 22 Feb 2021 13:43:57 +0100
Subject: [PATCH 105/389] Add <code> if it's missing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/messages/TextualBody.js | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 6b201f1b8e..4223b9cbb8 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -96,6 +96,14 @@ export default class TextualBody extends React.Component {
             const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
             if (pres.length > 0) {
                 for (let i = 0; i < pres.length; i++) {
+                    let code = pres[i].getElementsByTagName("code")[0];
+                    // Add code element if it's missing
+                    if (!code) {
+                        code = document.createElement("code");
+                        code.innerHTML = pres[i].innerHTML;
+                        pres[i].innerHTML = "";
+                        pres[i].appendChild(code);
+                    }
                     // If there already is a div wrapping the codeblock we want to skip this.
                     // This happens after the codeblock was edited.
                     if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;

From 428af8b9e294f0f93e41afe8a48a6b42b0253842 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 22 Feb 2021 16:48:12 +0000
Subject: [PATCH 106/389] Jitsi conferences names, take 3

Shorter, capatalised, just 'Jitsi' prefix rather than 'JitsiConference'
---
 src/CallHandler.tsx | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index f73424fd4d..ab3a601d93 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -85,7 +85,7 @@ import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapt
 import { Action } from './dispatcher/actions';
 import VoipUserMapper from './VoipUserMapper';
 import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
-import { randomString } from "matrix-js-sdk/src/randomstring";
+import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring";
 
 export const PROTOCOL_PSTN = 'm.protocol.pstn';
 export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
@@ -861,8 +861,9 @@ export default class CallHandler {
             // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
             confId = base32.stringify(Buffer.from(roomId), { pad: false });
         } else {
-            // Create a random human readable conference ID
-            confId = `JitsiConference${randomString(32)}`;
+            // Create a random conference ID
+            const random = randomUppercaseString(1) + randomLowercaseString(23);
+            confId = 'Jitsi' + random;
         }
 
         let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});

From effb9666f2187591d13c832c92e128e7662e0c53 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Mon, 22 Feb 2021 20:10:30 +0100
Subject: [PATCH 107/389] Fix portrait videocalls
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/voip/_VideoFeed.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss
index 931410dba3..3e473a80b2 100644
--- a/res/css/views/voip/_VideoFeed.scss
+++ b/res/css/views/voip/_VideoFeed.scss
@@ -16,6 +16,7 @@ limitations under the License.
 
 .mx_VideoFeed_remote {
     width: 100%;
+    max-height: 100%;
     background-color: #000;
     z-index: 50;
 }

From c965119410ffeb4be0a5d73d22bf593a51dcdecf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 23 Feb 2021 07:40:53 +0100
Subject: [PATCH 108/389] Add _addCodeElement method
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/messages/TextualBody.js | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 4223b9cbb8..c6352e0e67 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -96,14 +96,8 @@ export default class TextualBody extends React.Component {
             const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
             if (pres.length > 0) {
                 for (let i = 0; i < pres.length; i++) {
-                    let code = pres[i].getElementsByTagName("code")[0];
                     // Add code element if it's missing
-                    if (!code) {
-                        code = document.createElement("code");
-                        code.innerHTML = pres[i].innerHTML;
-                        pres[i].innerHTML = "";
-                        pres[i].appendChild(code);
-                    }
+                    if (!pres[i].getElementsByTagName("code")[0]) this._addCodeElement(pres[i]);
                     // If there already is a div wrapping the codeblock we want to skip this.
                     // This happens after the codeblock was edited.
                     if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
@@ -136,6 +130,13 @@ export default class TextualBody extends React.Component {
         }
     }
 
+    _addCodeElement(pre) {
+        const code = document.createElement("code");
+        code.innerHTML = pre.innerHTML;
+        pre.innerHTML = "";
+        pre.appendChild(code);
+    }
+
     _addCodeExpansionButton(div, pre) {
         // Calculate how many percent does the pre element take up.
         // If it's less than 30% we don't add the expansion button.

From c9baff1e101afb216fe75d56e1fa523cc8b1bfa5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 23 Feb 2021 07:46:03 +0100
Subject: [PATCH 109/389] Move the _addCodeElement() call down a bit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

We can skip this if the first if statement is true

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/messages/TextualBody.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index c6352e0e67..5be4a6bbb4 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -96,11 +96,13 @@ export default class TextualBody extends React.Component {
             const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
             if (pres.length > 0) {
                 for (let i = 0; i < pres.length; i++) {
-                    // Add code element if it's missing
-                    if (!pres[i].getElementsByTagName("code")[0]) this._addCodeElement(pres[i]);
                     // If there already is a div wrapping the codeblock we want to skip this.
                     // This happens after the codeblock was edited.
                     if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
+                    // Add code element if it's missing
+                    if (!pres[i].getElementsByTagName("code")[0]) {
+                        this._addCodeElement(pres[i]);
+                    }
                     // Wrap a div around <pre> so that the copy button can be correctly positioned
                     // when the <pre> overflows and is scrolled horizontally.
                     const div = this._wrapInDiv(pres[i]);

From ce1be7a4cdcace08bac8cd18a1569a59f58f4b9b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 23 Feb 2021 07:49:26 +0100
Subject: [PATCH 110/389] Use length instead of the first element
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/messages/TextualBody.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 5be4a6bbb4..63d287870e 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -100,7 +100,7 @@ export default class TextualBody extends React.Component {
                     // This happens after the codeblock was edited.
                     if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
                     // Add code element if it's missing
-                    if (!pres[i].getElementsByTagName("code")[0]) {
+                    if (pres[i].getElementsByTagName("code").length == 0) {
                         this._addCodeElement(pres[i]);
                     }
                     // Wrap a div around <pre> so that the copy button can be correctly positioned

From 3db6a450105a32ee6a1de8c735c6ab3f6bd906ba Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Tue, 23 Feb 2021 12:03:08 +0000
Subject: [PATCH 111/389] Revert "Merge pull request #5637 from
 williamkray/wreck/clean-thumbnail-changes"

This reverts commit 5c1b38a48cae9bf3511b36d2b0a417b45a953fc4, reversing
changes made to ec4a39a694aada1b2239d0a6ab236638585ac43f.
---
 res/css/views/messages/_MVideoBody.scss         |  2 +-
 res/css/views/rooms/_LinkPreviewWidget.scss     |  5 -----
 src/components/views/messages/MImageBody.js     |  2 +-
 src/components/views/rooms/LinkPreviewWidget.js | 10 +++++-----
 4 files changed, 7 insertions(+), 12 deletions(-)

diff --git a/res/css/views/messages/_MVideoBody.scss b/res/css/views/messages/_MVideoBody.scss
index 2be15447f7..ac3491bc8f 100644
--- a/res/css/views/messages/_MVideoBody.scss
+++ b/res/css/views/messages/_MVideoBody.scss
@@ -17,7 +17,7 @@ limitations under the License.
 span.mx_MVideoBody {
     video.mx_MVideoBody {
         max-width: 100%;
-        max-height: 300px;
+        height: auto;
         border-radius: 4px;
     }
 }
diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss
index 5310bd3bbb..022cf3ed28 100644
--- a/res/css/views/rooms/_LinkPreviewWidget.scss
+++ b/res/css/views/rooms/_LinkPreviewWidget.scss
@@ -19,8 +19,6 @@ limitations under the License.
     margin-right: 15px;
     margin-bottom: 15px;
     display: flex;
-    flex-direction: column;
-    max-width: 360px;
     border-left: 4px solid $preview-widget-bar-color;
     color: $preview-widget-fg-color;
 }
@@ -57,9 +55,6 @@ limitations under the License.
     cursor: pointer;
     width: 18px;
     height: 18px;
-    padding: 0px 5px 5px 5px;
-    margin-left: auto;
-    margin-right: 0px;
 
     img {
         flex: 0 0 40px;
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 8456a5bd09..a8cdc17abf 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -362,7 +362,7 @@ export default class MImageBody extends React.Component {
         }
 
         // The maximum height of the thumbnail as it is rendered as an <img>
-        const maxHeight = Math.min(this.props.maxImageHeight || 240, infoHeight);
+        const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
         // The maximum width of the thumbnail, as dictated by its natural
         // maximum height.
         const maxWidth = infoWidth * maxHeight / infoHeight;
diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js
index 458b3e7054..2a053bf467 100644
--- a/src/components/views/rooms/LinkPreviewWidget.js
+++ b/src/components/views/rooms/LinkPreviewWidget.js
@@ -107,7 +107,7 @@ export default class LinkPreviewWidget extends React.Component {
         if (!SettingsStore.getValue("showImages")) {
             image = null; // Don't render a button to show the image, just hide it outright
         }
-        const imageMaxWidth = 320; const imageMaxHeight = 240;
+        const imageMaxWidth = 100; const imageMaxHeight = 100;
         if (image && image.startsWith("mxc://")) {
             image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
         }
@@ -134,10 +134,6 @@ export default class LinkPreviewWidget extends React.Component {
         const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
         return (
             <div className="mx_LinkPreviewWidget">
-                <AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
-                    <img className="mx_filterFlipColor" alt="" role="presentation"
-                        src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
-                </AccessibleButton>
                 { img }
                 <div className="mx_LinkPreviewWidget_caption">
                     <div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
@@ -146,6 +142,10 @@ export default class LinkPreviewWidget extends React.Component {
                         { description }
                     </div>
                 </div>
+                <AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
+                    <img className="mx_filterFlipColor" alt="" role="presentation"
+                        src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
+                </AccessibleButton>
             </div>
         );
     }

From 0daed986275b34287a9ef8383f500cf07b93b093 Mon Sep 17 00:00:00 2001
From: Will Hunt <willh@matrix.org>
Date: Tue, 23 Feb 2021 12:51:17 +0000
Subject: [PATCH 112/389] Do not process an older usage event if we've already
 processed a newer one

---
 src/components/structures/LoggedInView.tsx | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index c76cd7cee7..77c846d816 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -108,6 +108,7 @@ interface IState {
         };
     };
     usageLimitEventContent?: IUsageLimit;
+    usageLimitEventTs?: number;
     useCompactLayout: boolean;
 }
 
@@ -339,9 +340,18 @@ class LoggedInView extends React.Component<IProps, IState> {
                 e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
             );
         });
+        if (this.state.usageLimitEventTs > usageLimitEvent.getTs()) {
+            // We've processed a newer event than this one, so ignore it.
+            return;
+        }
         const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
         this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
-        this.setState({ usageLimitEventContent });
+        this.setState({
+            usageLimitEventContent,
+            usageLimitEventTs: usageLimitEvent.getTs(),
+            // This is a fresh toast, we can show toasts again
+            usageLimitDismissed: false,
+        });
     };
 
     _onPaste = (ev) => {

From aa86c1d751087d0f49b624bbc3fc82589d6e69ac Mon Sep 17 00:00:00 2001
From: Will Hunt <willh@matrix.org>
Date: Tue, 23 Feb 2021 12:51:47 +0000
Subject: [PATCH 113/389] Ensure that a dismissed usage alert toast stays
 dismissed

---
 src/components/structures/LoggedInView.tsx | 19 +++++++++++++++++--
 src/toasts/ServerLimitToast.tsx            |  7 +++++--
 2 files changed, 22 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 77c846d816..60969e3220 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -107,6 +107,7 @@ interface IState {
             errcode: string;
         };
     };
+    usageLimitDismissed: boolean;
     usageLimitEventContent?: IUsageLimit;
     usageLimitEventTs?: number;
     useCompactLayout: boolean;
@@ -152,6 +153,7 @@ class LoggedInView extends React.Component<IProps, IState> {
             syncErrorData: undefined,
             // use compact timeline view
             useCompactLayout: SettingsStore.getValue('useCompactLayout'),
+            usageLimitDismissed: false,
         };
 
         // stash the MatrixClient in case we log out before we are unmounted
@@ -303,14 +305,27 @@ class LoggedInView extends React.Component<IProps, IState> {
         }
     };
 
+    _onHideToast = () => {
+        this.setState({
+            usageLimitDismissed: true,
+        });
+    }
+
     _calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
         const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
         if (error) {
             usageLimitEventContent = syncError.error.data;
         }
 
-        if (usageLimitEventContent) {
-            showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
+        // usageLimitDismissed is true when the user has explicitly hidden the toast
+        // and it will be reset to false if a *new* usage alert comes in.
+        if (usageLimitEventContent && this.state.usageLimitDismissed) {
+            showServerLimitToast(
+                usageLimitEventContent.limit_type,
+                this._onHideToast,
+                usageLimitEventContent.admin_contact,
+                error,
+            );
         } else {
             hideServerLimitToast();
         }
diff --git a/src/toasts/ServerLimitToast.tsx b/src/toasts/ServerLimitToast.tsx
index d35140be3d..068d62f9ea 100644
--- a/src/toasts/ServerLimitToast.tsx
+++ b/src/toasts/ServerLimitToast.tsx
@@ -23,7 +23,7 @@ import {messageForResourceLimitError} from "../utils/ErrorUtils";
 
 const TOAST_KEY = "serverlimit";
 
-export const showToast = (limitType: string, adminContact?: string, syncError?: boolean) => {
+export const showToast = (limitType: string, onHideToast: () => void, adminContact?: string, syncError?: boolean) => {
     const errorText = messageForResourceLimitError(limitType, adminContact, {
         'monthly_active_user': _td("Your homeserver has exceeded its user limit."),
         '': _td("Your homeserver has exceeded one of its resource limits."),
@@ -38,7 +38,10 @@ export const showToast = (limitType: string, adminContact?: string, syncError?:
         props: {
             description: <React.Fragment>{errorText} {contactText}</React.Fragment>,
             acceptLabel: _t("Ok"),
-            onAccept: hideToast,
+            onAccept: () => {
+                hideToast()
+                onHideToast()
+            },
         },
         component: GenericToast,
         priority: 70,

From 84bcdf66f2a02f0fa779a5e80646b5e108614439 Mon Sep 17 00:00:00 2001
From: Will Hunt <willh@matrix.org>
Date: Tue, 23 Feb 2021 13:00:05 +0000
Subject: [PATCH 114/389] Fix TS check

---
 src/components/structures/LoggedInView.tsx | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 60969e3220..f3cce3ef34 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -336,10 +336,12 @@ class LoggedInView extends React.Component<IProps, IState> {
         if (!serverNoticeList) return [];
 
         const events = [];
+        let pinnedEventTs = 0;
         for (const room of serverNoticeList) {
             const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
 
             if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
+            pinnedEventTs = pinStateEvent.getTs();
 
             const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
             for (const eventId of pinnedEventIds) {
@@ -349,21 +351,22 @@ class LoggedInView extends React.Component<IProps, IState> {
             }
         }
 
+        if (pinnedEventTs && this.state.usageLimitEventTs > pinnedEventTs) {
+            // We've processed a newer event than this one, so ignore it.
+            return;
+        }
+
         const usageLimitEvent = events.find((e) => {
             return (
                 e && e.getType() === 'm.room.message' &&
                 e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
             );
         });
-        if (this.state.usageLimitEventTs > usageLimitEvent.getTs()) {
-            // We've processed a newer event than this one, so ignore it.
-            return;
-        }
         const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
         this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
         this.setState({
             usageLimitEventContent,
-            usageLimitEventTs: usageLimitEvent.getTs(),
+            usageLimitEventTs: pinnedEventTs,
             // This is a fresh toast, we can show toasts again
             usageLimitDismissed: false,
         });

From 64945d4ccf7b565afd921130c1a88d58b7e7fb83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 23 Feb 2021 19:14:36 +0100
Subject: [PATCH 115/389] Add a comment and use rems
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_EventTile.scss | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 4db16217e7..6f7cd46d51 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -257,7 +257,9 @@ $left-gutter: 64px;
     display: inline-block;
     width: 14px;
     height: 14px;
-    top: -20px;
+    // This aligns the avatar with the last line of the 
+    // message. We want to move it one line up - 2.2rem
+    top: -2.2rem;
     user-select: none;
     z-index: 1;
 }

From 3f0e8e4afcb7b9e77ed40448319f29d5c3368fc8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 23 Feb 2021 19:16:52 +0100
Subject: [PATCH 116/389] Delint
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_EventTile.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 6f7cd46d51..5091d55c08 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -257,7 +257,7 @@ $left-gutter: 64px;
     display: inline-block;
     width: 14px;
     height: 14px;
-    // This aligns the avatar with the last line of the 
+    // This aligns the avatar with the last line of the
     // message. We want to move it one line up - 2.2rem
     top: -2.2rem;
     user-select: none;

From 61cd026d7aad7f51fce7f15d640943292279b119 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 24 Feb 2021 13:07:25 +0100
Subject: [PATCH 117/389] Improve comment

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

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 63d287870e..b4535287dd 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -99,7 +99,7 @@ export default class TextualBody extends React.Component {
                     // If there already is a div wrapping the codeblock we want to skip this.
                     // This happens after the codeblock was edited.
                     if (pres[i].parentNode.className == "mx_EventTile_pre_container") continue;
-                    // Add code element if it's missing
+                    // Add code element if it's missing since we depend on it
                     if (pres[i].getElementsByTagName("code").length == 0) {
                         this._addCodeElement(pres[i]);
                     }

From 363b753306d10d6949165fdef27e4eedaf4b1193 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 24 Feb 2021 13:40:37 +0100
Subject: [PATCH 118/389] Avoid innerHTML
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/messages/TextualBody.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 63d287870e..e60ab5b535 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -134,7 +134,7 @@ export default class TextualBody extends React.Component {
 
     _addCodeElement(pre) {
         const code = document.createElement("code");
-        code.innerHTML = pre.innerHTML;
+        code.append(...pre.childNodes);
         pre.innerHTML = "";
         pre.appendChild(code);
     }

From 986950697b964b3575dc81f1db8f7bfbb6965ef6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 24 Feb 2021 14:10:09 +0100
Subject: [PATCH 119/389] Rmove unnecessary code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/messages/TextualBody.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 7c18b8c142..04db7bd725 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -135,7 +135,6 @@ export default class TextualBody extends React.Component {
     _addCodeElement(pre) {
         const code = document.createElement("code");
         code.append(...pre.childNodes);
-        pre.innerHTML = "";
         pre.appendChild(code);
     }
 

From 4e87fdcdfef860a303042e7806e110a0de84b195 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 24 Feb 2021 13:40:22 +0000
Subject: [PATCH 120/389] Fix object diffing when objects have different keys

The object diff optimisation in 32cca0534c5ff7c7a86d854e6a00a4764652d20b is not
correct for the case where `b` has some keys that are not in `a`.

By ensuring their key arrays are same length, we can preserve optimisation and
be correct as well.

Fixes https://github.com/vector-im/element-web/issues/16514
---
 src/utils/objects.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/utils/objects.ts b/src/utils/objects.ts
index 166c31c4c3..e7f4f0f907 100644
--- a/src/utils/objects.ts
+++ b/src/utils/objects.ts
@@ -89,6 +89,7 @@ export function objectHasDiff<O extends {}>(a: O, b: O): boolean {
     if (a === b) return false;
     const aKeys = Object.keys(a);
     const bKeys = Object.keys(b);
+    if (aKeys.length !== bKeys.length) return true;
     const possibleChanges = arrayUnion(aKeys, bKeys);
     // if the amalgamation of both sets of keys has the a different length to the inputs then there must be a change
     if (possibleChanges.length !== aKeys.length) return true;

From 2e6d8e886de46b9407003443291a46c22de60c62 Mon Sep 17 00:00:00 2001
From: b068931cc450442b63f5b3d276ea4297
 <b068931cc450442b63f5b3d276ea4297@protonmail.com>
Date: Wed, 24 Feb 2021 16:00:07 +0000
Subject: [PATCH 121/389] Translated using Weblate (German)

Currently translated at 99.6% (2754 of 2764 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 | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index d92aa27e0a..04ec99c9d5 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1045,7 +1045,7 @@
     "Unicorn": "Einhorn",
     "Pig": "Schwein",
     "Elephant": "Elefant",
-    "Rabbit": "Kaninchen",
+    "Rabbit": "Hase",
     "Panda": "Panda",
     "Rooster": "Hahn",
     "Penguin": "Pinguin",
@@ -1073,7 +1073,7 @@
     "Hat": "Hut",
     "Glasses": "Brille",
     "Spanner": "Schraubenschlüssel",
-    "Santa": "Nikolaus",
+    "Santa": "Weihnachtsmann",
     "Thumbs up": "Daumen hoch",
     "Umbrella": "Regenschirm",
     "Hourglass": "Sanduhr",
@@ -1370,7 +1370,7 @@
     "%(num)s days from now": "in %(num)s Tagen",
     "Show info about bridges in room settings": "Information über Bridges in den Raumeinstellungen anzeigen",
     "Enable message search in encrypted rooms": "Nachrichtensuche in verschlüsselten Räumen aktivieren",
-    "Lock": "Sperren",
+    "Lock": "Schloss",
     "Later": "Später",
     "Review": "Überprüfen",
     "Verify": "Verifizieren",

From 20a3b6f4aacba1162bba0bf1b0d943db7e3d50a1 Mon Sep 17 00:00:00 2001
From: libexus <Asterixeins324@gmail.com>
Date: Tue, 23 Feb 2021 18:25:20 +0000
Subject: [PATCH 122/389] Translated using Weblate (German)

Currently translated at 99.6% (2754 of 2764 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 | 92 ++++++++++++++++++-------------------
 1 file changed, 46 insertions(+), 46 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 04ec99c9d5..b44515aaa7 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -35,7 +35,7 @@
     "Deactivate Account": "Benutzerkonto deaktivieren",
     "Failed to send email": "Fehler beim Senden der E-Mail",
     "Account": "Benutzerkonto",
-    "Click here to fix": "Zum reparieren hier klicken",
+    "Click here to fix": "Zum Reparieren hier klicken",
     "Default": "Standard",
     "Export E2E room keys": "E2E-Raum-Schlüssel exportieren",
     "Failed to change password. Is your password correct?": "Passwortänderung fehlgeschlagen. Ist dein Passwort richtig?",
@@ -83,7 +83,7 @@
     "Server may be unavailable, overloaded, or you hit a bug.": "Server ist nicht verfügbar, überlastet oder du bist auf einen Softwarefehler gestoßen.",
     "Labs": "Labor",
     "Unable to add email address": "E-Mail-Adresse konnte nicht hinzugefügt werden",
-    "Unable to remove contact information": "Die Kontakt-Informationen konnten nicht gelöscht werden",
+    "Unable to remove contact information": "Die Kontaktinformationen können nicht gelöscht werden",
     "Unable to verify email address.": "Die E-Mail-Adresse konnte nicht verifiziert werden.",
     "Unban": "Verbannung aufheben",
     "unknown error code": "Unbekannter Fehlercode",
@@ -311,7 +311,7 @@
     "No Microphones detected": "Keine Mikrofone erkannt",
     "No media permissions": "Keine Medienberechtigungen",
     "You may need to manually permit %(brand)s to access your microphone/webcam": "Gegebenenfalls kann es notwendig sein, dass du %(brand)s manuell den Zugriff auf dein Mikrofon bzw. deine Webcam gewähren musst",
-    "Default Device": "Standard-Gerät",
+    "Default Device": "Standardgerät",
     "Microphone": "Mikrofon",
     "Camera": "Kamera",
     "Export": "Exportieren",
@@ -644,7 +644,7 @@
     "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Um einen Filter zu setzen, ziehe ein Community-Bild auf das Filter-Panel ganz links. Du kannst jederzeit auf einen Avatar im Filter-Panel klicken, um nur die Räume und Personen aus der Community zu sehen.",
     "Clear filter": "Filter zurücksetzen",
     "Key request sent.": "Schlüssel-Anfragen gesendet.",
-    "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Wenn du einen Fehler via GitHub gemeldet hast, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer. Sie enthalten keine Nachrichten.",
+    "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Wenn du einen Fehler via GitHub meldest, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer:innen. Sie enthalten keine Nachrichten.",
     "Submit debug logs": "Fehlerberichte einreichen",
     "Code": "Code",
     "Opens the Developer Tools dialog": "Öffnet die Entwickler-Werkzeuge",
@@ -819,8 +819,8 @@
     "Link to selected message": "Link zur ausgewählten Nachricht",
     "COPY": "KOPIEREN",
     "Share Message": "Nachricht teilen",
-    "No Audio Outputs detected": "Keine Ton-Ausgabe erkannt",
-    "Audio Output": "Ton-Ausgabe",
+    "No Audio Outputs detected": "Keine Audioausgabe erkannt",
+    "Audio Output": "Audioausgabe",
     "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In verschlüsselten Räumen, wie diesem, ist die Link-Vorschau standardmäßig deaktiviert damit dein Heimserver (auf dem die Vorschau erzeugt wird) keine Informationen über Links in diesem Raum bekommt.",
     "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Wenn jemand eine URL in seine Nachricht einfügt, kann eine URL-Vorschau angezeigt werden, um mehr Informationen über diesen Link zu erhalten, wie z.B. den Titel, die Beschreibung und ein Bild von der Website.",
     "The email field must not be blank.": "Das E-Mail-Feld darf nicht leer sein.",
@@ -874,7 +874,7 @@
     "Please review and accept the policies of this homeserver:": "Bitte sieh dir alle Bedingungen dieses Heimservers an und akzeptiere sie:",
     "Add some now": "Jemanden jetzt hinzufügen",
     "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Du bist ein:e Administrator:in dieser Community. Du wirst nicht erneut hinzutreten können, wenn du nicht von einem/r anderen Administrator:in eingeladen wirst.",
-    "Open Devtools": "Öffne Entwickler-Werkzeuge",
+    "Open Devtools": "Entwicklerwerkzeuge öffnen",
     "Show developer tools": "Zeige Entwicklerwerkzeuge",
     "Unable to load! Check your network connectivity and try again.": "Konnte nicht geladen werden! Überprüfe die Netzwerkverbindung und versuche es erneut.",
     "Delete Backup": "Sicherung löschen",
@@ -1006,9 +1006,9 @@
     "Profile picture": "Profilbild",
     "Display Name": "Anzeigename",
     "Room information": "Rauminformationen",
-    "Internal room ID:": "Interne Raum ID:",
-    "Room version": "Raum Version",
-    "Room version:": "Raum-Version:",
+    "Internal room ID:": "Interne Raum-ID:",
+    "Room version": "Raumversion",
+    "Room version:": "Raumversion:",
     "Developer options": "Entwickleroptionen",
     "General": "Allgemein",
     "Set a new account password...": "Neues Benutzerkonto-Passwort festlegen...",
@@ -1103,7 +1103,7 @@
     "Timeline": "Chatverlauf",
     "Autocomplete delay (ms)": "Verzögerung zur Autovervollständigung (ms)",
     "Roles & Permissions": "Rollen & Berechtigungen",
-    "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Änderungen daran, wer den Chatverlauf lesen kann werden nur zukünftige Nachrichten in diesem Raum angewendet. Die Sichtbarkeit der existierenden Historie bleibt unverändert.",
+    "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Änderungen daran, wer den Chatverlauf lesen kann werden nur zukünftige Nachrichten in diesem Raum angewendet. Die Sichtbarkeit des existierenden Verlaufs bleibt unverändert.",
     "Security & Privacy": "Sicherheit & Datenschutz",
     "Encryption": "Verschlüsselung",
     "Once enabled, encryption cannot be disabled.": "Sobald aktiviert, kann die Verschlüsselung nicht mehr deaktiviert werden.",
@@ -1208,15 +1208,15 @@
     "Change topic": "Ändere das Thema",
     "Modify widgets": "Ändere Widgets",
     "Default role": "Standard Rolle",
-    "Send messages": "Sende Nachrichten",
+    "Send messages": "Nachrichten senden",
     "Invite users": "Benutzer:innen einladen",
-    "Change settings": "Ändere Einstellungen",
+    "Change settings": "Einstellungen ändern",
     "Kick users": "Benutzer:innen kicken",
     "Ban users": "Benutzer:innen verbannen",
     "Remove messages": "Nachrichten löschen",
-    "Notify everyone": "Jeden Benachrichtigen",
-    "Send %(eventType)s events": "Sende %(eventType)s-Ereignisse",
-    "Select the roles required to change various parts of the room": "Wähle Rollen die benötigt werden um einige Teile des Raumes zu ändern",
+    "Notify everyone": "Jeden benachrichtigen",
+    "Send %(eventType)s events": "%(eventType)s-Ereignisse senden",
+    "Select the roles required to change various parts of the room": "Wähle Rollen, die benötigt werden, um einige Teile des Raumes zu ändern",
     "Enable encryption?": "Verschlüsselung aktivieren?",
     "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Sobald aktiviert, kann die Verschlüsselung für einen Raum nicht mehr deaktiviert werden. Nachrichten in einem verschlüsselten Raum können nur noch von Teilnehmern aber nicht mehr vom Server gelesen werden. Einige Bots und Brücken werden vielleicht nicht mehr funktionieren. <a>Erfahre mehr über Verschlüsselung.</a>",
     "Error updating main address": "Fehler beim Aktualisieren der Hauptadresse",
@@ -1301,7 +1301,7 @@
     "Multiple integration managers": "Mehrere Integrationsmanager",
     "Public Name": "Öffentlicher Name",
     "Identity Server URL must be HTTPS": "Die Identity Server-URL muss HTTPS sein",
-    "Could not connect to Identity Server": "Verbindung zum Identity Server konnte nicht hergestellt werden",
+    "Could not connect to Identity Server": "Verbindung zum Identitätsserver konnte nicht hergestellt werden",
     "Checking server": "Server wird überprüft",
     "Identity server has no terms of service": "Der Identitätsserver hat keine Nutzungsbedingungen",
     "Disconnect": "Trennen",
@@ -1384,7 +1384,7 @@
     "Cannot connect to integration manager": "Verbindung zum Integrationsmanager fehlgeschlagen",
     "The integration manager is offline or it cannot reach your homeserver.": "Der Integrationsmanager ist offline oder er kann den Heimserver nicht erreichen.",
     "not stored": "nicht gespeichert",
-    "Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s": "Die Sicherung hat eine Signatur von <verify>unbekanntem/r</verify> Nutzer!n mit ID %(deviceId)s",
+    "Backup has a signature from <verify>unknown</verify> user with ID %(deviceId)s": "Die Sicherung hat eine Signatur von <verify>unbekanntem/r</verify> Nutzer:in mit ID %(deviceId)s",
     "Backup key stored: ": "Backup Schlüssel gespeichert: ",
     "Clear notifications": "Benachrichtigungen löschen",
     "Disconnect from the identity server <current /> and connect to <new /> instead?": "Vom Identitätsserver <current /> trennen, und stattdessen eine Verbindung zu <new /> aufbauen?",
@@ -1396,12 +1396,12 @@
     "You are still <b>sharing your personal data</b> on the identity server <idserver />.": "Du <b>teilst deine persönlichen Daten</b> immer noch auf dem Identitätsserver <idserver />.",
     "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Wir empfehlen, dass du deine Email Adressen und Telefonnummern vom Identitätsserver löschst, bevor du die Verbindung trennst.",
     "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Zur Zeit benutzt du keinen Identitätsserver. Trage unten einen Server ein, um Kontakte finden und von anderen gefunden zu werden.",
-    "Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Nutze einen Integrationsmanager <b>(%(serverName)s)</b> um Bots, Widgets und Sticker Packs zu verwalten.",
-    "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsmanager um Bots, Widgets und Sticker Packs zu verwalten.",
+    "Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Nutze einen Integrationsmanager <b>(%(serverName)s)</b>, um Bots, Widgets und Stickerpacks zu verwalten.",
+    "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsmanager, um Bots, Widgets und Sticker Packs zu verwalten.",
     "Manage integrations": "Integrationen verwalten",
-    "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Stimme den Nutzungsbedingungen des Identitätsservers %(serverName)s zu, um dich per Email Adresse und Telefonnummer auffindbar zu machen.",
+    "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Stimme den Nutzungsbedingungen des Identitätsservers %(serverName)s zu, um dich per E-Mail-Adresse und Telefonnummer auffindbar zu machen.",
     "Clear cache and reload": "Zwischenspeicher löschen und neu laden",
-    "Customise your experience with experimental labs features. <a>Learn more</a>.": "Passe deine Erfahrung mit experimentellen Lab Funktionen an. <a>Mehr erfahren</a>.",
+    "Customise your experience with experimental labs features. <a>Learn more</a>.": "Passe deine Erfahrung mit experimentellen Funktionen an. <a>Mehr erfahren</a>.",
     "Ignored/Blocked": "Ignoriert/Blockiert",
     "Something went wrong. Please try again or view your console for hints.": "Etwas ist schief gelaufen. Bitte versuche es erneut oder sieh für weitere Hinweise in deiner Konsole nach.",
     "Error subscribing to list": "Fehler beim Abonnieren der Liste",
@@ -1413,7 +1413,7 @@
     "You have not ignored anyone.": "Du hast niemanden ignoriert.",
     "You are currently ignoring:": "Du ignorierst momentan:",
     "Unsubscribe": "Deabonnieren",
-    "View rules": "Regeln betrachten",
+    "View rules": "Regeln öffnen",
     "You are currently subscribed to:": "Du abonnierst momentan:",
     "⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer:innen gedacht.",
     "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem Touch das primäre Eingabegerät ist",
@@ -1432,9 +1432,9 @@
     "Never send encrypted messages to unverified sessions from this session": "Sende niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen",
     "Never send encrypted messages to unverified sessions in this room from this session": "Sende niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen in diesem Raum",
     "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Durch die Änderung des Passworts werden derzeit alle Ende-zu-Ende-Verschlüsselungsschlüssel in allen Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist, es sei denn, du exportierst zuerst deine Raumschlüssel und importierst sie anschließend wieder. In Zukunft wird dies verbessert werden.",
-    "Delete %(count)s sessions|other": "Lösche %(count)s Sitzungen",
+    "Delete %(count)s sessions|other": "%(count)s Sitzungen löschen",
     "Backup is not signed by any of your sessions": "Die Sicherung wurde von keiner deiner Sitzungen unterzeichnet",
-    "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du erhälst keine Push-Benachrichtigungen zu anderen Sitzungen, bis du dich wieder bei diesen anmeldst",
+    "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du erhältst keine Push-Benachrichtigungen zu anderen Sitzungen, bis du dich wieder bei diesen anmeldest",
     "Notification sound": "Benachrichtigungston",
     "Set a new custom sound": "Setze einen neuen benutzerdefinierten Ton",
     "Browse": "Durchsuche",
@@ -1455,9 +1455,9 @@
     "Notification Autocomplete": "Benachrichtigung Autovervollständigen",
     "If disabled, messages from encrypted rooms won't appear in search results.": "Wenn deaktiviert, werden Nachrichten von verschlüsselten Räumen nicht in den Ergebnissen auftauchen.",
     "This user has not verified all of their sessions.": "Diese:r Benutzer:in hat nicht alle seine/ihre Sitzungen verifiziert.",
-    "You have verified this user. This user has verified all of their sessions.": "Du hast diese/n Nutzer!n verifiziert. Er/Sie hat alle seine/ihre Sitzungen verifiziert.",
+    "You have verified this user. This user has verified all of their sessions.": "Du hast diese/n Nutzer:in verifiziert. Er/Sie hat alle seine/ihre Sitzungen verifiziert.",
     "Your key share request has been sent - please check your other sessions for key share requests.": "Deine Schlüsselanfrage wurde gesendet - sieh in deinen anderen Sitzungen nach der Schlüsselanfrage.",
-    "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast klicke hier, um die Schlüssel erneut anzufordern.",
+    "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast, klicke hier, um die Schlüssel erneut anzufordern.",
     "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Wenn deine anderen Sitzungen nicht über den Schlüssel für diese Nachricht verfügen, kannst du die Nachricht nicht entschlüsseln.",
     "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Fordere die Verschlüsselungsschlüssel aus deinen anderen Sitzungen erneut an</requestLink>.",
     "Room %(name)s": "Raum %(name)s",
@@ -1589,12 +1589,12 @@
     "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s ändert eine Ausschluss-Regel für Räume von %(oldGlob)s nach %(newGlob)s, wegen %(reason)s",
     "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Auf den Server turn.matrix.org zurückgreifen, falls deine Heimserver keine Anruf-Assistenz anbietet (deine IP-Adresse wird während eines Anrufs geteilt)",
     "Show more": "Mehr zeigen",
-    "This session is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "Diese Sitzung <b>sichert nicht deine Schlüssel</b>, aber du hast eine vorhandene Sicherung, die du wiederherstellen und in Zukunft hinzufügen kannst.",
+    "This session is <b>not backing up your keys</b>, but you do have an existing backup you can restore from and add to going forward.": "Diese Sitzung <b>sichert deine Schlüssel nicht</b>, aber du hast eine vorhandene Sicherung, die du wiederherstellen und in Zukunft hinzufügen kannst.",
     "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Verbinde diese Sitzung mit deiner Schlüsselsicherung bevor du dich abmeldest, um den Verlust von Schlüsseln zu vermeiden.",
     "This backup is trusted because it has been restored on this session": "Dieser Sicherung wird vertraut, da sie während dieser Sitzung wiederhergestellt wurde",
     "Enable desktop notifications for this session": "Desktop-Benachrichtigungen für diese Sitzung aktivieren",
     "Enable audible notifications for this session": "Aktiviere die akustischen Benachrichtigungen für diese Sitzung",
-    "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsmanaager erhalten Konfigurationsdaten und können Widgets modifizieren, Raumeinladungen verschicken und in deinem Namen Einflusslevel setzen.",
+    "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsmanager erhalten Konfigurationsdaten und können Widgets modifizieren, Raumeinladungen verschicken und in deinem Namen Berechtigungslevel setzen.",
     "Read Marker lifetime (ms)": "Gültigkeitsdauer der Gelesen-Markierung (ms)",
     "Read Marker off-screen lifetime (ms)": "Gültigkeitsdauer der Gelesen-Markierung außerhalb des Bildschirms (ms)",
     "Session key:": "Sitzungsschlüssel:",
@@ -1679,8 +1679,8 @@
     "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Bestätige das Löschen dieser Sitzung indem du dich mittels Single Sign-On anmeldest um deine Identität nachzuweisen.",
     "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Bestätige das Löschen dieser Sitzung indem du dich mittels Single Sign-On anmeldest um deine Identität nachzuweisen.",
     "Confirm deleting these sessions": "Bestätige das Löschen dieser Sitzungen",
-    "Click the button below to confirm deleting these sessions.|other": "Klicke den Button um das Löschen dieser Sitzungen zu bestätigen.",
-    "Click the button below to confirm deleting these sessions.|one": "Klicke den Button um das Löschen dieser Sitzung zu bestätigen.",
+    "Click the button below to confirm deleting these sessions.|other": "Klicke den Knopf, um das Löschen dieser Sitzungen zu bestätigen.",
+    "Click the button below to confirm deleting these sessions.|one": "Klicke den Knopf, um das Löschen dieser Sitzung zu bestätigen.",
     "Clear all data in this session?": "Alle Daten dieser Sitzung löschen?",
     "Clear all data": "Alle Daten löschen",
     "Confirm your account deactivation by using Single Sign On to prove your identity.": "Bestätige das Löschen deines Kontos indem du dich mittels Single Sign-On anmeldest um deine Identität nachzuweisen.",
@@ -1702,15 +1702,15 @@
     "Error adding ignored user/server": "Fehler beim Hinzufügen eines ignorierten Nutzers/Servers",
     "None": "Keine",
     "Ban list rules - %(roomName)s": "Verbotslistenregeln - %(roomName)s",
-    "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Füge hier Benutzer!nnen und Server hinzu, die du ignorieren willst. Verwende Sternchen, damit %(brand)s mit beliebigen Zeichen übereinstimmt. Bspw. würde <code>@bot: *</code> alle Benutzer!nnen ignorieren, die auf einem Server den Namen 'bot' haben.",
-    "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignorieren von Personen erfolgt über Sperrlisten. Wenn eine Sperrliste abonniert wird, werden die von dieser Liste blockierten Benutzer!nnen/Server ausgeblendet.",
+    "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Füge hier Benutzer!nnen und Server hinzu, die du ignorieren willst. Verwende Sternchen, damit %(brand)s mit beliebigen Zeichen übereinstimmt. Bspw. würde <code>@bot: *</code> alle Benutzer:innen ignorieren, die auf einem Server den Namen 'bot' haben.",
+    "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Das Ignorieren von Personen erfolgt über Sperrlisten. Wenn eine Sperrliste abonniert wird, werden die von dieser Liste blockierten Benutzer:innen und Server ausgeblendet.",
     "Personal ban list": "Persönliche Sperrliste",
-    "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Deine persönliche Sperrliste enthält alle Benutzer!nnen/Server, von denen du persönlich keine Nachrichten sehen willst. Nachdem du den ersten Benutzer/Server ignoriert hast, wird in der Raumliste \"Meine Sperrliste\" angezeigt - bleibe in diesem Raum, um die Sperrliste aufrecht zu halten.",
+    "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Deine persönliche Sperrliste enthält alle Benutzer:innen/Server, von denen du persönlich keine Nachrichten sehen willst. Nachdem du den ersten Benutzer/Server ignoriert hast, wird in der Raumliste \"Meine Sperrliste\" angezeigt - bleibe in diesem Raum, um die Sperrliste aufrecht zu halten.",
     "Server or user ID to ignore": "Zu ignorierende Server- oder Benutzer-ID",
     "eg: @bot:* or example.org": "z.B. @bot:* oder example.org",
     "Subscribed lists": "Abonnierte Listen",
     "Subscribing to a ban list will cause you to join it!": "Eine Verbotsliste abonnieren bedeutet ihr beizutreten!",
-    "If this isn't what you want, please use a different tool to ignore users.": "Wenn dies nicht das ist, was du willst, verwende ein anderes Tool, um Benutzer!nnen zu ignorieren.",
+    "If this isn't what you want, please use a different tool to ignore users.": "Wenn dies nicht das ist, was du willst, verwende ein anderes Tool, um Benutzer:innen zu ignorieren.",
     "Subscribe": "Abonnieren",
     "Always show the window menu bar": "Fenstermenüleiste immer anzeigen",
     "Show tray icon and minimize window to it on close": "Taskleistensymbol anzeigen und Fenster beim Schließen dorthin minimieren",
@@ -1752,8 +1752,8 @@
     "in account data": "in den Kontodaten",
     "Homeserver feature support:": "Home-Server-Funktionsunterstützung:",
     "exists": "existiert",
-    "Delete sessions|other": "Lösche Sitzungen",
-    "Delete sessions|one": "Lösche Sitzung",
+    "Delete sessions|other": "Sitzungen löschen",
+    "Delete sessions|one": "Sitzung löschen",
     "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Alle Sitzungen einzeln verifizieren, anstatt auch Sitzungen zu vertrauen, die durch Cross-Signing verifiziert sind.",
     "Securely cache encrypted messages locally for them to appear in search results, using ": "Der Zwischenspeicher für die lokale Suche in verschlüsselten Nachrichten benötigt ",
     " to store messages from ": " um Nachrichten von ",
@@ -1766,7 +1766,7 @@
     "Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> session <device></device>": "Die Sicherung hat eine <validity>ungültige</validity> Signatur von einer <verify>nicht verifizierten</verify> Sitzung <device></device>",
     "Your keys are <b>not being backed up from this session</b>.": "Deine Schlüssel werden <b>nicht von dieser Sitzung gesichert</b>.",
     "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Zur Zeit verwendest du <server></server>, um Kontakte zu finden und von anderen gefunden zu werden. Du kannst deinen Identitätsserver weiter unten ändern.",
-    "Invalid theme schema.": "Ungültiges Design Schema.",
+    "Invalid theme schema.": "Ungültiges Designschema.",
     "Error downloading theme information.": "Fehler beim herunterladen des Themas.",
     "Theme added!": "Design hinzugefügt!",
     "Custom theme URL": "URL des benutzerdefinierten Designs",
@@ -1781,11 +1781,11 @@
     "Complete": "Abschließen",
     "Revoke": "Widerrufen",
     "Share": "Teilen",
-    "You have not verified this user.": "Du hast diese:n Nutzer!n nicht verifiziert.",
-    "Everyone in this room is verified": "Jede/r in diesem Raum ist verifiziert",
+    "You have not verified this user.": "Du hast diese:n Nutzer:in nicht verifiziert.",
+    "Everyone in this room is verified": "Alle in diesem Raum sind verifiziert",
     "Mod": "Mod",
     "Invite only": "Nur auf Einladung",
-    "Scroll to most recent messages": "Springe zur neusten Nachricht",
+    "Scroll to most recent messages": "Zur neusten Nachricht springen",
     "No recent messages by %(user)s found": "Keine neuen Nachrichten von %(user)s gefunden",
     "Try scrolling up in the timeline to see if there are any earlier ones.": "Versuche nach oben zu scrollen, um zu sehen ob sich dort frühere Nachrichten befinden.",
     "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Dies kann bei vielen Nachrichten einige Zeit dauern. Bitte lade die Anwendung in dieser Zeit nicht neu.",
@@ -2136,7 +2136,7 @@
     "Jump to room search": "Springe zur Raumsuche",
     "Close dialog or context menu": "Schließe Dialog oder Kontextmenü",
     "Cancel autocomplete": "Deaktiviere Auto-Vervollständigung",
-    "Unable to revoke sharing for email address": "Das Teilen der E-Mail-Adresse kann nicht widerrufen werden",
+    "Unable to revoke sharing for email address": "Dem Teilen der E-Mail-Adresse kann nicht widerrufen werden",
     "Unable to validate homeserver/identity server": "Heimserver/Identitätsserver nicht validierbar",
     "Without completing security on this session, it won’t have access to encrypted messages.": "Ohne Abschluss der Sicherungseinrichtung in dieser Sitzung wird sie keinen Zugriff auf verschlüsselte Nachrichten erhalten.",
     "Disable": "Deaktivieren",
@@ -2333,7 +2333,7 @@
     "Incoming voice call": "Eingehender Sprachanruf",
     "Incoming video call": "Eingehender Videoanruf",
     "Incoming call": "Eingehender Anruf",
-    "There are advanced notifications which are not shown here.": "Erweiterte Benachrichtigungen, werden hier nicht angezeigt.",
+    "There are advanced notifications which are not shown here.": "Erweiterte Benachrichtigungen werden hier nicht angezeigt.",
     "Are you sure you want to cancel entering passphrase?": "Bist du sicher, dass du die Eingabe der Passphrase abbrechen möchtest?",
     "Use your account to sign in to the latest version": "Melde dich mit deinem Account in der neuesten Version an",
     "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s",
@@ -2378,7 +2378,7 @@
     "A connection error occurred while trying to contact the server.": "Beim Versuch, den Server zu kontaktieren, ist ein Verbindungsfehler aufgetreten.",
     "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Du hast sie ggf. in einem anderen Client als %(brand)s konfiguriert. Du kannst sie nicht in %(brand)s verändern, aber sie werden trotzdem angewandt.",
     "Master private key:": "Privater Hauptschlüssel:",
-    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart & %(brand)s werden versuchen sie zu verwenden.",
+    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Setze den Schriftnamen auf eine in deinem System installierte Schriftart & %(brand)s wird versuchen, sie zu verwenden.",
     "Custom Tag": "Benutzerdefinierter Tag",
     "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at <a>element.io/get-started</a>.": "Du bist bereits eingeloggt und kannst loslegen. Allerdings kannst du auch die neuesten Versionen der App für alle Plattformen unter <a>element.io/get-started</a> herunterladen.",
     "You're all caught up.": "Alles gesichtet.",
@@ -2619,8 +2619,8 @@
     "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.",
+    "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. Es werden %(size)s benötigt, 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. Es werden %(size)s benötigt, 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>)",

From 28cd3095333cafa9a2294cf770c2474e6355c817 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Tue, 23 Feb 2021 10:47:36 +0000
Subject: [PATCH 123/389] Translated using Weblate (German)

Currently translated at 99.6% (2754 of 2764 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 | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index b44515aaa7..3f657e105b 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -909,7 +909,7 @@
     "All-uppercase is almost as easy to guess as all-lowercase": "Alles groß zu geschrieben ist fast genauso schnell zu raten, wie alles klein zu schreiben",
     "Reversed words aren't much harder to guess": "Umgedrehte Worte sind nicht schwerer zu erraten",
     "Predictable substitutions like '@' instead of 'a' don't help very much": "Vorhersagbare Ersetzungen wie '@' anstelle von 'a' helfen nicht viel",
-    "Add another word or two. Uncommon words are better.": "Füge ein weiteres Wort hinzu - oder mehr. Ungewöhnliche Worte sind besser.",
+    "Add another word or two. Uncommon words are better.": "Füge ein weiteres Wort - oder mehr - hinzu. Ungewöhnliche Worte sind besser.",
     "Repeats like \"aaa\" are easy to guess": "Wiederholungen wie \"aaa\" sind einfach zu erraten",
     "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Wiederholungen wie \"abcabcabc\" sind nur leicht schwerer zu raten als \"abc\"",
     "Sequences like abc or 6543 are easy to guess": "Sequenzen wie \"abc\" oder \"6543\" sind leicht zu raten",
@@ -987,7 +987,7 @@
     "Enable big emoji in chat": "Aktiviere große Emoji im Chat",
     "Enable Community Filter Panel": "Community-Filter-Panel aktivieren",
     "Messages containing my username": "Nachrichten, die meinen Benutzernamen enthalten",
-    "The other party cancelled the verification.": "Die Gegenstelle hat die Verifizierung abgebrochen.",
+    "The other party cancelled the verification.": "Die Gegenstelle hat die Überprüfung abgebrochen.",
     "Verified!": "Verifiziert!",
     "You've successfully verified this user.": "Du hast diese:n Benutzer:in erfolgreich verifiziert.",
     "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sichere Nachrichten mit diesem/r Benutzer:in sind Ende-zu-Ende-verschlüsselt und können nicht von Dritten gelesen werden.",
@@ -1111,7 +1111,7 @@
     "Ignored users": "Ignorierte Benutzer",
     "Key backup": "Schlüsselsicherung",
     "Gets or sets the room topic": "Frage das Thema des Raums ab oder setze es",
-    "Verify this user by confirming the following emoji appear on their screen.": "Verifiziere diese Nutzer:in, indem du bestätigst, dass folgendes Emoji auf dessen Bildschirm erscheint.",
+    "Verify this user by confirming the following emoji appear on their screen.": "Verifiziere diese Nutzer:in, indem du bestätigst, dass folgende Emojis auf dessen Bildschirm erscheinen.",
     "Missing media permissions, click the button below to request.": "Fehlende Medienberechtigungen. Drücke auf den Knopf unten, um sie anzufordern.",
     "Request media permissions": "Medienberechtigungen anfordern",
     "Main address": "Primäre Adresse",
@@ -2202,7 +2202,7 @@
     "I want to help": "Ich möchte helfen",
     "Your homeserver has exceeded its user limit.": "Dein Heimserver hat das Benutzerlimit erreicht.",
     "Your homeserver has exceeded one of its resource limits.": "Dein Heimserver hat eine seiner Ressourcengrenzen erreicht.",
-    "Contact your <a>server admin</a>.": "Kontaktiere deinen <a>Heimserver-Administrator</a>.",
+    "Contact your <a>server admin</a>.": "Kontaktiere deine <a>Heimserver-Administration</a>.",
     "Ok": "Ok",
     "Set password": "Setze Passwort",
     "To return to your account in future you need to set a password": "Um dein Konto zukünftig wieder verwenden zu können, setze ein Passwort",

From 49d8158c7e8eb4660abc2c26a67121b3bcc98e9e Mon Sep 17 00:00:00 2001
From: iaiz <git@iapellaniz.com>
Date: Tue, 16 Feb 2021 16:52:11 +0000
Subject: [PATCH 124/389] Translated using Weblate (Spanish)

Currently translated at 96.4% (2667 of 2764 strings)

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

diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json
index 64de246c0e..cbe7e0a223 100644
--- a/src/i18n/strings/es.json
+++ b/src/i18n/strings/es.json
@@ -13,8 +13,8 @@
     "A new password must be entered.": "Debes ingresar una contraseña nueva.",
     "%(senderName)s answered the call.": "%(senderName)s contestó la llamada.",
     "An error has occurred.": "Un error ha ocurrido.",
-    "Anyone who knows the room's link, apart from guests": "Cualquier persona que conozca el enlace a esta sala, excepto huéspedes",
-    "Anyone who knows the room's link, including guests": "Cualquier persona que conozca el enlace a esta sala, incluyendo huéspedes",
+    "Anyone who knows the room's link, apart from guests": "Cualquier persona que conozca el enlace a esta sala, pero excluir a la gente sin cuenta",
+    "Anyone who knows the room's link, including guests": "Cualquier persona que conozca el enlace a esta sala, incluyendo gente sin cuenta",
     "Are you sure?": "¿Estás seguro?",
     "Are you sure you want to reject the invitation?": "¿Estás seguro que quieres rechazar la invitación?",
     "Attachment": "Adjunto",
@@ -24,7 +24,7 @@
     "Banned users": "Usuarios vetados",
     "Bans user with given id": "Veta al usuario con la ID dada",
     "Call Timeout": "Tiempo de Espera de Llamada",
-    "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>.": "No se puede conectar al servidor base a través de HTTP, cuando es necesario un enlace HTTPS en la barra de direcciones de tu navegador. Ya sea usando HTTPS o <a>habilitando los scripts inseguros</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>.": "No se ha podido conectar al servidor base a través de HTTP, cuando es necesario un enlace HTTPS en la barra de direcciones de tu navegador. Ya sea usando HTTPS o <a>activando los scripts inseguros</a>.",
     "Change Password": "Cambiar contraseña",
     "%(senderName)s changed their profile picture.": "%(senderName)s cambió su imagen de perfil.",
     "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s ha cambiado el nivel de acceso de %(powerLevelDiffText)s.",
@@ -215,7 +215,7 @@
     "No users have specific privileges in this room": "Ningún usuario tiene permisos específicos en esta sala",
     "OK": "Vale",
     "olm version:": "versión de olm:",
-    "Only people who have been invited": "Solo personas que han sido invitadas",
+    "Only people who have been invited": "Solo las personas que hayan sido invitadas",
     "Operation failed": "Falló la operación",
     "Password": "Contraseña",
     "Passwords can't be empty": "Las contraseñas no pueden estar en blanco",
@@ -278,7 +278,7 @@
     "Unable to verify email address.": "No es posible verificar la dirección de correo electrónico.",
     "Unban": "Quitar Veto",
     "Unable to capture screen": "No es posible capturar la pantalla",
-    "Unable to enable Notifications": "No es posible habilitar las Notificaciones",
+    "Unable to enable Notifications": "No se han podido activar las notificaciones",
     "unknown caller": "Persona que llama desconocida",
     "Unnamed Room": "Sala sin nombre",
     "Uploading %(filename)s and %(count)s others|zero": "Subiendo %(filename)s",
@@ -314,14 +314,14 @@
     "AM": "AM",
     "PM": "PM",
     "The maximum permitted number of widgets have already been added to this room.": "La cantidad máxima de widgets permitida ha sido alcanzada en esta sala.",
-    "To use it, just wait for autocomplete results to load and tab through them.": "Para utilizarlo, tan solo espera a que se carguen los resultados de autocompletar y navega entre ellos.",
+    "To use it, just wait for autocomplete results to load and tab through them.": "Para usarlo, tan solo espera a que se carguen los resultados de autocompletar y navega entre ellos.",
     "%(senderName)s unbanned %(targetName)s.": "%(senderName)s le quitó el veto a %(targetName)s.",
     "Unmute": "Dejar de silenciar",
     "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (nivel de permisos %(powerLevelNumber)s)",
     "You cannot place VoIP calls in this browser.": "No puedes realizar llamadas VoIP en este navegador.",
     "You do not have permission to post to this room": "No tienes permiso para publicar en esta sala",
-    "You have <a>disabled</a> URL previews by default.": "Ha <a>deshabilitado</a> la vista previa de URL por defecto.",
-    "You have <a>enabled</a> URL previews by default.": "Ha <a>habilitado</a> vista previa de URL por defecto.",
+    "You have <a>disabled</a> URL previews by default.": "Has <a>desactivado</a> la vista previa de URLs por defecto.",
+    "You have <a>enabled</a> URL previews by default.": "Has <a>activado</a> las vista previa de URLs por defecto.",
     "You have no visible notifications": "No tiene notificaciones visibles",
     "You must <a>register</a> to use this functionality": "Usted debe ser un <a>registrar</a> para usar esta funcionalidad",
     "You need to be able to invite users to do that.": "Debes ser capaz de invitar usuarios para realizar esa acción.",
@@ -374,7 +374,7 @@
     "Sunday": "Domingo",
     "Guests can join": "Los invitados se pueden unir",
     "Failed to add tag %(tagName)s to room": "Error al añadir la etiqueta %(tagName)s a la sala",
-    "Notification targets": "Objetivos de notificación",
+    "Notification targets": "Destinos de notificaciones",
     "Failed to set direct chat tag": "Error al establecer la etiqueta de conversación directa",
     "Today": "Hoy",
     "Files": "Archivos",
@@ -388,7 +388,7 @@
     "Leave": "Salir",
     "Uploaded on %(date)s by %(user)s": "Subido el %(date)s por %(user)s",
     "Send Custom Event": "Enviar evento personalizado",
-    "All notifications are currently disabled for all targets.": "Las notificaciones están deshabilitadas para todos los objetivos.",
+    "All notifications are currently disabled for all targets.": "Las notificaciones están desactivadas para todos los objetivos.",
     "Failed to send logs: ": "Error al enviar registros: ",
     "Forget": "Olvidar",
     "World readable": "Legible por todo el mundo",
@@ -409,19 +409,19 @@
     "An error occurred whilst saving your email notification preferences.": "Se ha producido un error al guardar las preferencias de notificación por email.",
     "Explore Room State": "Explorar Estado de la Sala",
     "Source URL": "URL de Origen",
-    "Messages sent by bot": "Mensajes enviados por bot",
+    "Messages sent by bot": "Mensajes enviados por bots",
     "Filter results": "Filtrar resultados",
     "Members": "Miembros",
     "No update available.": "No hay actualizaciones disponibles.",
-    "Noisy": "Ruidoso",
+    "Noisy": "Sonoro",
     "Collecting app version information": "Recolectando información de la versión de la aplicación",
     "Keywords": "Palabras clave",
-    "Enable notifications for this account": "Habilitar notificaciones para esta cuenta",
+    "Enable notifications for this account": "Activar notificaciones para esta cuenta",
     "Invite to this community": "Invitar a esta comunidad",
     "Messages containing <span>keywords</span>": "Mensajes que contienen <span>palabras clave</span>",
     "Error saving email notification preferences": "Error al guardar las preferencias de notificación por email",
     "Tuesday": "Martes",
-    "Enter keywords separated by a comma:": "Introduzca palabras clave separadas por una coma:",
+    "Enter keywords separated by a comma:": "Escribe palabras clave separadas por una coma:",
     "Search…": "Buscar…",
     "You have successfully set a password and an email address!": "¡Has establecido una nueva contraseña y dirección de correo electrónico!",
     "Remove %(name)s from the directory?": "¿Eliminar a %(name)s del directorio?",
@@ -448,14 +448,14 @@
     "Send": "Enviar",
     "Send logs": "Enviar registros",
     "All messages": "Todos los mensajes",
-    "Call invitation": "Invitación a la llamada",
+    "Call invitation": "Cuando me inviten a una llamada",
     "Thank you!": "¡Gracias!",
     "Downloading update...": "Descargando actualizaciones...",
     "State Key": "Clave de estado",
     "Failed to send custom event.": "Ha fallado el envio del evento personalizado.",
     "What's new?": "¿Qué hay de nuevo?",
     "Notify me for anything else": "Notificarme para cualquier otra cosa",
-    "When I'm invited to a room": "Cuando soy invitado a una sala",
+    "When I'm invited to a room": "Cuando me inviten a una sala",
     "Can't update user notification settings": "No se puede actualizar los ajustes de notificaciones del usuario",
     "Notify for all other messages/rooms": "Notificar para todos los demás mensajes/salas",
     "Unable to look up room ID from server": "No se puede buscar el ID de la sala desde el servidor",
@@ -469,7 +469,7 @@
     "Reply": "Responder",
     "Show message in desktop notification": "Mostrar mensaje en la notificación del escritorio",
     "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Los registros de depuración contienen datos de uso de la aplicación como nombre de usuario, ID o alias de las salas o grupos que hayas visitado (y nombres de usuario de otros usuarios). No contienen mensajes.",
-    "Unhide Preview": "Mostrar Vista Previa",
+    "Unhide Preview": "Mostrar vista previa",
     "Unable to join network": "No se puede unir a la red",
     "Sorry, your browser is <b>not</b> able to run %(brand)s.": "¡Lo sentimos! Su navegador <b>no puede</b> ejecutar %(brand)s.",
     "Messages in group chats": "Mensajes en conversaciones grupales",
@@ -478,12 +478,12 @@
     "Low Priority": "Prioridad baja",
     "%(brand)s does not know how to join a room on this network": "%(brand)s no sabe cómo unirse a una sala en esta red",
     "Set Password": "Establecer contraseña",
-    "Off": "Desactivado",
+    "Off": "Apagado",
     "Mentions only": "Solo menciones",
     "Failed to remove tag %(tagName)s from room": "Error al eliminar la etiqueta %(tagName)s de la sala",
     "Wednesday": "Miércoles",
     "You can now return to your account after signing out, and sign in on other devices.": "Ahora puedes regresar a tu cuenta después de cerrar tu sesión, e iniciar sesión en otros dispositivos.",
-    "Enable email notifications": "Habilitar notificaciones por email",
+    "Enable email notifications": "Activar notificaciones por correo",
     "Event Type": "Tipo de Evento",
     "No rooms to show": "No hay salas para mostrar",
     "Download this file": "Descargar este archivo",
@@ -493,7 +493,7 @@
     "Developer Tools": "Herramientas de desarrollo",
     "View Source": "Ver fuente",
     "Event Content": "Contenido del Evento",
-    "Unable to fetch notification target list": "No se puede obtener la lista de objetivos de notificación",
+    "Unable to fetch notification target list": "No se puede obtener la lista de destinos de notificación",
     "Quote": "Citar",
     "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "En su navegador actual, la apariencia y comportamiento de la aplicación puede ser completamente incorrecta, y algunas de las características podrían no funcionar. Si aún desea probarlo puede continuar, pero ¡no podremos ofrecer soporte por cualquier problema que pudiese tener!",
     "Checking for an update...": "Comprobando actualizaciones...",
@@ -540,8 +540,8 @@
     "Automatically replace plain text Emoji": "Reemplazar automáticamente texto por Emojis",
     "Mirror local video feed": "Clonar transmisión de video local",
     "Send analytics data": "Enviar datos de análisis de estadísticas",
-    "Enable inline URL previews by default": "Habilitar vistas previas de URL en línea por defecto",
-    "Enable URL previews for this room (only affects you)": "Activar vista previa de URL en esta sala (sólo le afecta a ud.)",
+    "Enable inline URL previews by default": "Activar vistas previas de URLs en línea por defecto",
+    "Enable URL previews for this room (only affects you)": "Activar vista previa de URL en esta sala (te afecta a ti)",
     "Enable URL previews by default for participants in this room": "Activar vista previa de URL por defecto para los participantes en esta sala",
     "Enable widget screenshots on supported widgets": "Activar capturas de pantalla de widget en los widgets soportados",
     "Drop file here to upload": "Soltar aquí el fichero a subir",
@@ -593,21 +593,21 @@
     "Members only (since the point in time of selecting this option)": "Solo miembros (desde el momento en que se selecciona esta opción)",
     "Members only (since they were invited)": "Solo miembros (desde que fueron invitados)",
     "Members only (since they joined)": "Solo miembros (desde que se unieron)",
-    "You don't currently have any stickerpacks enabled": "Actualmente no tienes ningún paquete de pegatinas habilitado",
+    "You don't currently have any stickerpacks enabled": "Actualmente no tienes ningún paquete de pegatinas activado",
     "Stickerpack": "Paquete de pegatinas",
     "Hide Stickers": "Ocultar Pegatinas",
     "Show Stickers": "Mostrar Pegatinas",
     "Invalid community ID": "ID de comunidad inválida",
     "'%(groupId)s' is not a valid community ID": "'%(groupId)s' no es una ID de comunidad válida",
     "Flair": "Insignia",
-    "Showing flair for these communities:": "Mostrar insignias de estas comunidades:",
+    "Showing flair for these communities:": "Mostrar insignias de las siguientes comunidades:",
     "This room is not showing flair for any communities": "Esta sala no está mostrando insignias para ninguna comunidad",
     "New community ID (e.g. +foo:%(localDomain)s)": "Nueva ID de comunidad (ej. +foo:%(localDomain)s)",
     "URL previews are enabled by default for participants in this room.": "La vista previa de URL se activa por defecto en los participantes de esta sala.",
     "URL previews are disabled by default for participants in this room.": "La vista previa se desactiva por defecto para los participantes de esta sala.",
     "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "En salas cifradas como ésta, la vista previa de las URL se desactiva por defecto para asegurar que el servidor base (donde se generan) no puede recopilar información de los enlaces que veas en esta sala.",
     "URL Previews": "Vista previa de URL",
-    "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Cuando alguien pone una URL en su mensaje, una vista previa se mostrará para ofrecer información sobre el enlace, tal como título, descripción, y una imagen del sitio Web.",
+    "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Cuando alguien incluye una URL en su mensaje, se mostrará una vista previa para ofrecer información sobre el enlace, que incluirá el título, descripción, y una imagen del sitio web.",
     "Error decrypting audio": "Error al descifrar el sonido",
     "Error decrypting image": "Error al descifrar imagen",
     "Error decrypting video": "Error al descifrar video",
@@ -927,14 +927,14 @@
     "Custom user status messages": "Mensajes de estado de usuario personalizados",
     "Group & filter rooms by custom tags (refresh to apply changes)": "Agrupa y filtra salas por etiquetas personalizadas (refresca para aplicar cambios)",
     "Render simple counters in room header": "Muestra contadores simples en la cabecera de la sala",
-    "Enable Emoji suggestions while typing": "Habilitar sugerencia de Emojis mientras se teclea",
+    "Enable Emoji suggestions while typing": "Activar sugerencias de emojis al escribir",
     "Show a placeholder for removed messages": "Mostrar una marca para los mensaje borrados",
     "Show join/leave messages (invites/kicks/bans unaffected)": "Mostrar mensajes de entrada/salida (no afecta a invitaciones/expulsiones/baneos)",
     "Show avatar changes": "Mostrar cambios de avatar",
     "Show display name changes": "Muestra cambios en los nombres",
     "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Mostrar un recordatorio para habilitar 'Recuperación Segura de Mensajes' en sala cifradas",
     "Show avatars in user and room mentions": "Mostrar avatares en menciones a usuarios y salas",
-    "Enable big emoji in chat": "Habilitar emojis grandes en el chat",
+    "Enable big emoji in chat": "Activar emojis grandes en el chat",
     "Send typing notifications": "Enviar notificaciones de tecleo",
     "Allow Peer-to-Peer for 1:1 calls": "Permitir conexión de pares en llamadas individuales",
     "Prompt before sending invites to potentially invalid matrix IDs": "Pedir confirmación antes de enviar invitaciones a IDs de matrix que parezcan inválidas",
@@ -1033,7 +1033,7 @@
     "Profile picture": "Foto de perfil",
     "Display Name": "Nombre a mostrar",
     "Internal room ID:": "ID de Sala Interna:",
-    "Open Devtools": "Abrir Devtools",
+    "Open Devtools": "Abrir devtools",
     "General": "General",
     "Room Addresses": "Direcciones de sala",
     "Set a new account password...": "Establecer una nueva contraseña para la cuenta...",
@@ -1050,11 +1050,11 @@
     "Preferences": "Opciones",
     "Room list": "Lista de salas",
     "Autocomplete delay (ms)": "Retardo autocompletado (ms)",
-    "Roles & Permissions": "Roles & Permisos",
+    "Roles & Permissions": "Roles y permisos",
     "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Los cambios que se hagan sobre quien puede leer el historial se aplicarán solo a nuevos mensajes en esta sala. La visibilidad del historial actual no cambiará.",
     "Security & Privacy": "Seguridad y privacidad",
     "Encryption": "Cifrado",
-    "Once enabled, encryption cannot be disabled.": "Después de activarlo, el cifrado no se puede desactivar.",
+    "Once enabled, encryption cannot be disabled.": "Una vez activado, el cifrado no se puede desactivar.",
     "Encrypted": "Cifrado",
     "Ignored users": "Usuarios ignorados",
     "Bulk options": "Opciones generales",
@@ -1088,7 +1088,7 @@
     "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s cambió la regla para unirse a %(rule)s",
     "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s cambió el acceso para invitados a %(rule)s",
     "Use a longer keyboard pattern with more turns": "Usa un patrón de tecleo largo con más vueltas",
-    "Enable Community Filter Panel": "Habilitar el Panel de Filtro de Comunidad",
+    "Enable Community Filter Panel": "Activar el panel de filtro de comunidad",
     "Verify this user by confirming the following emoji appear on their screen.": "Verifica este usuario confirmando que los siguientes emojis aparecen en su pantalla.",
     "Your %(brand)s is misconfigured": "Tu %(brand)s tiene un error de configuración",
     "Whether or not you're logged in (we don't record your username)": "Hayas o no iniciado sesión (no guardamos tu nombre de usuario)",
@@ -1107,9 +1107,9 @@
     "%(senderName)s made no change.": "%(senderName)s no hizo ningún cambio.",
     "Sends the given message coloured as a rainbow": "Envía el mensaje coloreado como un arcoiris",
     "Sends the given emote coloured as a rainbow": "Envía el emoji coloreado como un arcoiris",
-    "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s ha habilitado las insignias para %(groups)s en esta sala.",
-    "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s ha deshabilitado las insignias para %(groups)s en esta sala.",
-    "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s ha habilitado las insignias para %(newGroups)s y las ha deshabilitado para %(oldGroups)s en esta sala.",
+    "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s ha activado las insignias para %(groups)s en esta sala.",
+    "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s ha desactivado las insignias para %(groups)s en esta sala.",
+    "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s ha activado las insignias para %(newGroups)s y las ha desactivado para %(oldGroups)s en esta sala.",
     "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s ha revocado la invitación para que %(targetDisplayName)s se una a la sala.",
     "Cannot reach homeserver": "No se puede conectar con el servidor",
     "Ensure you have a stable internet connection, or get in touch with the server admin": "Asegúrate de tener conexión a internet, o contacta con el administrador del servidor",
@@ -1162,7 +1162,7 @@
     "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s hizo una llamada de vídeo (no soportada por este navegador)",
     "%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
     "Try out new ways to ignore people (experimental)": "Pruebe nuevas formas de ignorar a usuarios (experimental)",
-    "Match system theme": "Utilizar el mismo tema que el sistema",
+    "Match system theme": "Usar el mismo tema que el sistema",
     "Show previews/thumbnails for images": "Mostrar vistas previas para las imágenes",
     "When rooms are upgraded": "Cuando las salas son actualizadas",
     "My Ban List": "Mi lista de baneos",
@@ -1236,8 +1236,8 @@
     "Suggestions": "Sugerencias",
     "Recently Direct Messaged": "Enviado Mensaje Directo recientemente",
     "Go": "Ir",
-    "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.": "Has usado %(brand)s anteriormente en %(host)s con carga diferida de usuarios habilitada. En esta versión la carga diferida está deshabilitada. Como el caché local no es compatible entre estas dos configuraciones, %(brand)s necesita resincronizar tu cuenta.",
-    "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Si la otra versión de %(brand)s esta todavía abierta en otra pestaña, por favor, ciérrala, ya que usar %(brand)s en el mismo host con la opción de carga diferida habilitada y deshabilitada simultáneamente causará problemas.",
+    "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.": "Has usado %(brand)s anteriormente en %(host)s con carga diferida de usuarios activada. En esta versión la carga diferida está desactivada. Como el caché local no es compatible entre estas dos configuraciones, %(brand)s tiene que resincronizar tu cuenta.",
+    "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Si la otra versión de %(brand)s esta todavía abierta en otra pestaña, por favor, ciérrala, ya que usar %(brand)s en el mismo host con la opción de carga diferida activada y desactivada simultáneamente causará problemas.",
     "Incompatible local cache": "Caché local incompatible",
     "Clear cache and resync": "Limpiar la caché y resincronizar",
     "I don't want my encrypted messages": "No quiero mis mensajes cifrados",
@@ -1348,7 +1348,7 @@
     "Show typing notifications": "Mostrar notificaciones de escritura",
     "Never send encrypted messages to unverified sessions from this session": "No enviar nunca mensajes cifrados a sesiones sin verificar desde esta sesión",
     "Never send encrypted messages to unverified sessions in this room from this session": "No enviar nunca mensajes cifrados a sesiones sin verificar en esta sala desde esta sesión",
-    "Enable message search in encrypted rooms": "Habilitar la búsqueda de mensajes en salas cifradas",
+    "Enable message search in encrypted rooms": "Activar la búsqueda de mensajes en salas cifradas",
     "How fast should messages be downloaded.": "Con qué rapidez deben ser descargados los mensajes.",
     "Verify this session by completing one of the following:": "Verifica esta sesión completando uno de los siguientes:",
     "Scan this unique code": "Escanea este código único",
@@ -1376,7 +1376,7 @@
     "Sounds": "Sonidos",
     "Notification sound": "Sonido de notificación",
     "Set a new custom sound": "Usar un nuevo sonido personalizado",
-    "Browse": "Navegar",
+    "Browse": "Seleccionar",
     "Change room avatar": "Cambiar avatar de sala",
     "Change room name": "Cambiar nombre de sala",
     "Change main address for the room": "Cambiar la dirección principal para la sala",
@@ -1400,7 +1400,7 @@
     "Notify everyone": "Notificar a todos",
     "Send %(eventType)s events": "Enviar eventos %(eventType)s",
     "Select the roles required to change various parts of the room": "Selecciona los roles requeridos para cambiar varias partes de la sala",
-    "Enable encryption?": "¿Habilitar cifrado?",
+    "Enable encryption?": "¿Activar cifrado?",
     "Your email address hasn't been verified yet": "Tu dirección de email no ha sido verificada",
     "Verify the link in your inbox": "Verifica el enlace en tu bandeja de entrada",
     "Complete": "Completar",
@@ -1411,8 +1411,8 @@
     "Backup key stored: ": "Clave de seguridad almacenada: ",
     "Your keys are <b>not being backed up from this session</b>.": "<b>No se está haciendo una copia de seguridad de tus claves en esta sesión</b>.",
     "Clear notifications": "Limpiar notificaciones",
-    "Enable desktop notifications for this session": "Habilitar notificaciones de escritorio para esta sesión",
-    "Enable audible notifications for this session": "Habilitar notificaciones sonoras para esta sesión",
+    "Enable desktop notifications for this session": "Activar notificaciones de escritorio para esta sesión",
+    "Enable audible notifications for this session": "Activar notificaciones sonoras para esta sesión",
     "Checking server": "Comprobando servidor",
     "Change identity server": "Cambiar servidor de identidad",
     "Disconnect from the identity server <current /> and connect to <new /> instead?": "¿Desconectarse del servidor de identidad <current /> y conectarse a <new/>?",
@@ -1525,7 +1525,7 @@
     "Backup has a <validity>valid</validity> signature from <verify>unverified</verify> session <device></device>": "La copia de seguridad tiene una firma de <validity>válida</validity> de sesión <verify>no verificada</verify> <device></device>",
     "Backup has an <validity>invalid</validity> signature from <verify>verified</verify> session <device></device>": "La copia de seguridad tiene una firma de <validity>no válida</validity> de sesión <verify>verificada</verify> <device></device>",
     "Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> session <device></device>": "La copia de seguridad tiene una firma de <validity>no válida</validity> de sesión <verify>no verificada</verify> <device></device>",
-    "<a>Upgrade</a> to your own domain": "<a>Actualizar</a> a su propio dominio",
+    "<a>Upgrade</a> to your own domain": "<a>Contratar</a> el uso de un dominio personalizado",
     "Identity Server URL must be HTTPS": "La URL del servidor de identidad debe ser tipo HTTPS",
     "Not a valid Identity Server (status code %(code)s)": "No es un servidor de identidad válido (código de estado %(code)s)",
     "Could not connect to Identity Server": "No se ha podido conectar al servidor de identidad",
@@ -1559,7 +1559,7 @@
     "Subscribe": "Suscribir",
     "Always show the window menu bar": "Siempre mostrar la barra de menú de la ventana",
     "Show tray icon and minimize window to it on close": "Mostrar el icono en el Área de Notificación y minimizar la ventana al cerrarla",
-    "Composer": "Compositor",
+    "Composer": "Editor",
     "Timeline": "Línea de tiempo",
     "Read Marker lifetime (ms)": "Permanencia del marcador de lectura (en ms)",
     "Read Marker off-screen lifetime (ms)": "Permanencia del marcador de lectura fuera de la pantalla (en ms)",
@@ -1641,12 +1641,12 @@
     "Please enter a name for the room": "Por favor, introduzca un nombre para la sala",
     "This room is private, and can only be joined by invitation.": "Esta sala es privada, y sólo se puede acceder a ella por invitación.",
     "Enable end-to-end encryption": "Activar el cifrado de extremo a extremo",
-    "You can’t disable this later. Bridges & most bots won’t work yet.": "No puedes deshabilitar esto después. Los puentes y la mayoría de los bots no funcionarán todavía.",
+    "You can’t disable this later. Bridges & most bots won’t work yet.": "No puedes desactivar esto después. Los puentes y la mayoría de los bots no funcionarán todavía.",
     "Create a public room": "Crear una sala pública",
     "Create a private room": "Crear una sala privada",
     "Topic (optional)": "Tema (opcional)",
     "Make this room public": "Convierte esta sala en pública",
-    "Hide advanced": "Ocultar opciones avanzadas",
+    "Hide advanced": "Ocultar ajustes avanzados",
     "Show advanced": "Mostrar ajustes avanzados",
     "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Evitar que usuarios de otros servidores Matrix se unan a esta sala (¡Este ajuste no puede ser cambiada más tarde!)",
     "Server did not require any authentication": "El servidor no requirió ninguna autenticación",
@@ -1663,10 +1663,10 @@
     "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Verificar que este usuario marcará su sesión como de confianza, y también que marcará su sesión como de confianza para él.",
     "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Verifique este dispositivo para marcarlo como confiable. Confiar en este dispositivo le da a usted y a otros usuarios tranquilidad adicional cuando utilizan mensajes cifrados de extremo a extremo.",
     "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "La verificación de este dispositivo lo marcará como de confianza. Los usuarios que te han verificado confiarán en este dispositivo.",
-    "Integrations are disabled": "Las integraciones están deshabilitadas",
-    "Enable 'Manage Integrations' in Settings to do this.": "Habilita 'Gestionar Integraciones' en Ajustes para hacer esto.",
+    "Integrations are disabled": "Las integraciones están desactivadas",
+    "Enable 'Manage Integrations' in Settings to do this.": "Activa «Gestionar integraciones» en ajustes para hacer esto.",
     "Integrations not allowed": "Integraciones no están permitidas",
-    "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Su %(brand)s no le permite utilizar un \"Administrador de Integración\" para hacer esto. Por favor, contacte con un administrador.",
+    "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "%(brand)s no utilizar un \"gestor de integración\" para hacer esto. Por favor, contacta con un administrador.",
     "Failed to invite the following users to chat: %(csvUsers)s": "Error invitando a los siguientes usuarios al chat: %(csvUsers)s",
     "We couldn't create your DM. Please check the users you want to invite and try again.": "No se ha podido crear el mensaje directo. Por favor, comprueba los usuarios que quieres invitar e inténtalo de nuevo.",
     "Start a conversation with someone using their name, username (like <userId/>) or email address.": "Iniciar una conversación con alguien usando su nombre, nombre de usuario (como <userId/>) o dirección de correo electrónico.",
@@ -1677,7 +1677,7 @@
     "a key signature": "un firma de clave",
     "%(brand)s encountered an error during upload of:": "%(brand)s encontró un error durante la carga de:",
     "End": "Fin",
-    "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Una vez habilitado, el cifrado de una sala no puede deshabilitarse. Los mensajes enviados a una sala cifrada no pueden ser vistos por el servidor, sólo lo verán los participantes de la sala. Habilitar el cifrado puede hacer que muchos bots y bridges no funcionen correctamente. <a>Aprende más de cifrado</a>",
+    "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Una vez activado, el cifrado de una sala no puede desactivarse. Los mensajes enviados a una sala cifrada no pueden ser vistos por el servidor, solo lo verán los participantes de la sala. Activar el cifrado puede hacer que muchos bots y bridges no funcionen correctamente. <a>Más información sobre el cifrado</a>",
     "Joining room …": "Uniéndose a sala …",
     "Loading …": "Cargando …",
     "Rejecting invite …": "Rechazando invitación …",
@@ -1762,7 +1762,7 @@
     "Published Addresses": "Direcciones publicadas",
     "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.": "Las direcciones publicadas pueden ser usadas por cualquier usuario en cualquier servidor para unirse a tu salas. Para publicar una dirección, primero hay que establecerla como una dirección local.",
     "Other published addresses:": "Otras direcciones publicadas:",
-    "No other published addresses yet, add one below": "No tiene direcciones publicadas todavía, agregue una más abajo",
+    "No other published addresses yet, add one below": "Todavía no hay direcciones publicadas, puedes añadir una más abajo",
     "New published address (e.g. #alias:server)": "Nueva dirección publicada (p.ej.. #alias:server)",
     "Local Addresses": "Direcciones locales",
     "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Establecer las direcciones de esta sala para que los usuarios puedan encontrarla a través de tu servidor base (%(localDomain)s)",
@@ -1925,7 +1925,7 @@
     "Resend edit": "Reenviar la edición",
     "Resend %(unsentCount)s reaction(s)": "Reenviar %(unsentCount)s reacción(es)",
     "Resend removal": "Reenviar la eliminación",
-    "Share Permalink": "Compartir Permalink",
+    "Share Permalink": "Compartir enlace",
     "Report Content": "Reportar contenido",
     "Notification settings": "Configuración de notificaciones",
     "Clear status": "Borrar estado",
@@ -2004,10 +2004,10 @@
     "%(brand)s failed to get the public room list.": "%(brand)s no logró obtener la lista de salas públicas.",
     "The homeserver may be unavailable or overloaded.": "Es posible el servidor de base no esté disponible o esté sobrecargado.",
     "Preview": "Ver",
-    "View": "Vista",
+    "View": "Ver",
     "Find a room…": "Encuentre una sala…",
-    "Find a room… (e.g. %(exampleRoom)s)": "Encontrar una sala una sala... (ej.: %(exampleRoom)s)",
-    "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Si no puedes encontrar la sala que buscas, pide que te inviten a ella o <a>crea una nueva</a>.",
+    "Find a room… (e.g. %(exampleRoom)s)": "Encuentra una sala... (ej.: %(exampleRoom)s)",
+    "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Si no encuentras la sala que buscas, pide que te inviten a ella o <a>crea una nueva</a>.",
     "Explore rooms": "Explorar salas",
     "Jump to first invite.": "Salte a la primera invitación.",
     "Add room": "Añadir sala",
@@ -2100,10 +2100,10 @@
     "This session is encrypting history using the new recovery method.": "Esta sesión está cifrando el historial usando el nuevo método de recuperación.",
     "Change notification settings": "Cambiar los ajustes de notificaciones",
     "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Prototipo de comunidades v2. Requiere un servidor compatible. Altamente experimental - usar con precuación.",
-    "Font size": "Tamaño de la fuente",
-    "Use custom size": "Utilizar un tamaño personalizado",
+    "Font size": "Tamaño del texto",
+    "Use custom size": "Usar un tamaño personalizado",
     "Use a more compact ‘Modern’ layout": "Usar un diseño más 'moderno' y compacto",
-    "Use a system font": "Utilizar una fuente del sistema",
+    "Use a system font": "Usar una fuente del sistema",
     "System font name": "Nombre de la fuente",
     "Enable experimental, compact IRC style layout": "Activar el diseño experimental de IRC compacto",
     "Uploading logs": "Subiendo registros",
@@ -2123,11 +2123,11 @@
     "Message layout": "Diseño del mensaje",
     "Compact": "Compacto",
     "Modern": "Moderno",
-    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Inserta el nombre de la fuente instalada en tu sistema y %(brand)s intentara utilizarla.",
-    "Customise your appearance": "Personaliza tu apariencia",
+    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Introduce el nombre de la fuente instalada en tu sistema y %(brand)s intentará utilizarla.",
+    "Customise your appearance": "Personaliza la apariencia",
     "Appearance Settings only affect this %(brand)s session.": "Cambiar las opciones de apariencia solo afecta esta %(brand)s sesión.",
     "Please verify the room ID or address and try again.": "Por favor, verifica la ID o dirección de esta sala e inténtalo de nuevo.",
-    "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "El administrador del servidor base ha deshabilitado el cifrado de extremo a extremo en salas privadas y mensajes directos.",
+    "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "El administrador del servidor base ha desactivado el cifrado de extremo a extremo en salas privadas y mensajes directos.",
     "To link to this room, please add an address.": "Para vincular esta sala, por favor añade una dirección.",
     "The authenticity of this encrypted message can't be guaranteed on this device.": "La autenticidad de este mensaje cifrado no puede ser garantizada en este dispositivo.",
     "No recently visited rooms": "No hay salas visitadas recientemente",
@@ -2144,7 +2144,7 @@
     "New spinner design": "Nuevo diseño de ruleta",
     "Show message previews for reactions in DMs": "Mostrar vistas previas de mensajes para reacciones en DM",
     "Show message previews for reactions in all rooms": "Mostrar vistas previas de mensajes para reacciones en todas las salas",
-    "Enable advanced debugging for the room list": "Habilite la depuración avanzada para la lista de salas",
+    "Enable advanced debugging for the room list": "Activar la depuración avanzada para la lista de salas",
     "IRC display name width": "Ancho del nombre de visualización de IRC",
     "Unknown caller": "Llamador desconocido",
     "Cross-signing is ready for use.": "La firma cruzada está lista para su uso.",
@@ -2220,7 +2220,7 @@
     "Send %(count)s invites|other": "Enviar %(count)s invitaciones",
     "Send %(count)s invites|one": "Enviar invitación a %(count)s",
     "Invite people to join %(communityName)s": "Invita a personas a unirse %(communityName)s",
-    "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Hubo un error al crear tu comunidad. El nombre puede ser tomado o el servidor no puede procesar su solicitud.",
+    "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Ha ocurrido un error al crear la comunidad. El nombre puede que ya esté siendo usado o el servidor no puede procesar la solicitud.",
     "Community ID: +<localpart />:%(domain)s": "ID de comunidad: +<localpart />:%(domain)s",
     "Use this when referencing your community to others. The community ID cannot be changed.": "Use esto cuando haga referencia a su comunidad con otras. La identificación de la comunidad no se puede cambiar.",
     "You can change this later if needed.": "Puede cambiar esto más tarde si es necesario.",
@@ -2235,7 +2235,7 @@
     "Create a room in %(communityName)s": "Crea una sala en %(communityName)s",
     "Block anyone not part of %(serverName)s from ever joining this room.": "Bloquea a cualquier persona que no sea parte de %(serverName)s para que no se una a esta sala.",
     "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Anteriormente usaste una versión más nueva de %(brand)s con esta sesión. Para volver a utilizar esta versión con cifrado de extremo a extremo, deberá cerrar sesión y volver a iniciar sesión.",
-    "There was an error updating your community. The server is unable to process your request.": "Hubo un error al actualizar tu comunidad. El servidor no puede procesar su solicitud.",
+    "There was an error updating your community. The server is unable to process your request.": "Ha ocurrido un error al actualizar tu comunidad. El servidor no puede procesar la solicitud.",
     "Update community": "Actualizar comunidad",
     "To continue, use Single Sign On to prove your identity.": "Para continuar, utilice el inicio de sesión único para demostrar su identidad.",
     "Confirm to continue": "Confirmar para continuar",
@@ -2246,10 +2246,10 @@
     "Server isn't responding": "El servidor no responde",
     "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Su servidor no responde a algunas de sus solicitudes. A continuación se presentan algunas de las razones más probables.",
     "The server (%(serverName)s) took too long to respond.": "El servidor (%(serverName)s) tardó demasiado en responder.",
-    "Your firewall or anti-virus is blocking the request.": "Su firewall o antivirus está bloqueando la solicitud.",
-    "A browser extension is preventing the request.": "Una extensión del navegador impide la solicitud.",
+    "Your firewall or anti-virus is blocking the request.": "Tu firewall o antivirus está bloqueando la solicitud.",
+    "A browser extension is preventing the request.": "Una extensión del navegador está impidiendo que se haga la solicitud.",
     "The server is offline.": "El servidor está desconectado.",
-    "The server has denied your request.": "El servidor ha denegado su solicitud.",
+    "The server has denied your request.": "El servidor ha rechazado la solicitud.",
     "Your area is experiencing difficulties connecting to the internet.": "Su área está experimentando dificultades para conectarse a Internet.",
     "A connection error occurred while trying to contact the server.": "Se produjo un error de conexión al intentar contactar con el servidor.",
     "The server is not configured to indicate what the problem is (CORS).": "El servidor no está configurado para indicar cuál es el problema (CORS).",
@@ -2396,7 +2396,7 @@
     "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.": "Si hizo esto accidentalmente, puede configurar Mensajes seguros en esta sesión que volverá a cifrar el historial de mensajes de esta sesión con un nuevo método de recuperación.",
     "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si no eliminó el método de recuperación, es posible que un atacante esté intentando acceder a su cuenta. Cambie la contraseña de su cuenta y configure un nuevo método de recuperación inmediatamente en Configuración.",
     "If disabled, messages from encrypted rooms won't appear in search results.": "Si está desactivado, los mensajes de las salas cifradas no aparecerán en los resultados de búsqueda.",
-    "Disable": "Inhabilitar",
+    "Disable": "Desactivar",
     "Not currently indexing messages for any room.": "Actualmente no indexa mensajes para ninguna sala.",
     "Currently indexing: %(currentRoom)s": "Actualmente indexando: %(currentRoom)s",
     "%(brand)s is securely caching encrypted messages locally for them to appear in search results:": "%(brand)s está almacenando en caché de forma segura los mensajes cifrados localmente para que aparezcan en los resultados de búsqueda:",
@@ -2419,8 +2419,8 @@
     "Toggle Quote": "Alternar Entrecomillar",
     "New line": "Nueva línea",
     "Navigate recent messages to edit": "Navegar por mensajes recientes para editar",
-    "Jump to start/end of the composer": "Saltar al inicio / final del compositor",
-    "Navigate composer history": "Navegar por el historial del compositor",
+    "Jump to start/end of the composer": "Saltar al inicio o final del editor",
+    "Navigate composer history": "Navegar por el historial del editor",
     "Cancel replying to a message": "Cancelar la respuesta a un mensaje",
     "Toggle microphone mute": "Alternar silencio del micrófono",
     "Toggle video on/off": "Activar/desactivar video",

From 69b7daae449793504a3cc3401397b8cd1e79c1b1 Mon Sep 17 00:00:00 2001
From: Thibault Martin <mail@thibaultmart.in>
Date: Wed, 24 Feb 2021 09:49:01 +0000
Subject: [PATCH 125/389] Translated using Weblate (French)

Currently translated at 100.0% (2764 of 2764 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 | 122 ++++++++++++++++++++++++++++++++++++---
 1 file changed, 115 insertions(+), 7 deletions(-)

diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index 3bb86d2c63..b98edea1b1 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -2547,7 +2547,7 @@
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Les salons privés ne peuvent être trouvés et rejoints seulement par invitation. Les salons publics peut être trouvés et rejoints par n'importe qui dans cette communauté.",
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Les salons privés ne peuvent être trouvés et rejoints seulement par invitation. Les salons publics peut être trouvés et rejoints par n'importe qui.",
     "Start a new chat": "Commencer une nouvelle discussion",
-    "Add a photo so people know it's you.": "Ajoutez une photo pour que les gens savent que c'est vous",
+    "Add a photo so people know it's you.": "Ajoutez une photo pour que les gens sachent qu’il s’agit de vous.",
     "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s ou %(usernamePassword)s",
     "Decide where your account is hosted": "Décidez où votre compte est hébergé",
     "Go to Home View": "Revenir à la page d'accueil",
@@ -2820,11 +2820,11 @@
     "British Indian Ocean Territory": "Territoire britannique de l'océan Indien",
     "Brazil": "Brésil",
     "Bouvet Island": "Île Bouvet",
-    "Botswana": "",
+    "Botswana": "Botswana",
     "Bosnia": "Bosnie-Herzegovine",
-    "Bolivia": "",
-    "Bhutan": "",
-    "Bermuda": "",
+    "Bolivia": "Bolivie",
+    "Bhutan": "Bhoutan",
+    "Bermuda": "Bermudes",
     "with state key %(stateKey)s": "avec la ou les clés d'état %(stateKey)s",
     "with an empty state key": "avec une clé d'état vide",
     "See when anyone posts a sticker to your active room": "Voir quand n'importe qui envoye un sticker dans le salon actuel",
@@ -2837,7 +2837,7 @@
     "See when the topic changes in this room": "Voir quand le sujet change dans ce salon",
     "See when the topic changes in your active room": "Voir quand le sujet change dans le salon actuel",
     "Change the name of your active room": "Changer le nom du salon actuel",
-    "See when the name changes in this room": "Traquer quand le nom change dans ce salon",
+    "See when the name changes in this room": "Suivre quand le nom de ce salon change",
     "Change the name of this room": "Changer le nom de ce salon",
     "Change the topic of your active room": "Changer le sujet dans le salon actuel",
     "Change the topic of this room": "Changer le sujet de ce salon",
@@ -2935,5 +2935,113 @@
     "See general files posted to your active room": "Voir les fichiers postés dans votre salon actuel",
     "See general files posted to this room": "Voir les fichiers postés dans ce salon",
     "Send general files as you in your active room": "Envoie des fichiers en tant que vous dans votre salon actuel",
-    "Send general files as you in this room": "Envoie des fichiers en tant que vous dans ce salon"
+    "Send general files as you in this room": "Envoie des fichiers en tant que vous dans ce salon",
+    "Search (must be enabled)": "Recherche (si activée)",
+    "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Cette session a détecté que votre phrase de passe et clé de sécurité pour les messages sécurisés ont été supprimées.",
+    "A new Security Phrase and key for Secure Messages have been detected.": "Une nouvelle phrase de passe et clé pour les messages sécurisés ont été détectées.",
+    "Make a copy of your Security Key": "Faire une copie de votre Clé de Sécurité",
+    "Confirm your Security Phrase": "Confirmez votre phrase de passe",
+    "Secure your backup with a Security Phrase": "Protégez votre sauvegarde avec une Clé de Sécurité",
+    "Your Security Key is in your <b>Downloads</b> folder.": "Votre Clé de Sécurité est dans le répertoire <b>Téléchargements</b>.",
+    "Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Votre Clé de Sécurité a été <b>copiée dans votre presse-papier</b>, copiez la pour :",
+    "Your Security Key": "Votre Clé de Sécurité",
+    "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Votre Clé de Sécurité est un filet de sécurité. Vous pouvez l’utiliser pour retrouver l’accès à vos messages chiffrés si vous oubliez votre phrase de passe.",
+    "Repeat your Security Phrase...": "Répétez votre phrase de passe…",
+    "Please enter your Security Phrase a second time to confirm.": "Merci de saisir votre phrase de passe une seconde fois pour confirmer.",
+    "Set up with a Security Key": "Configurer avec une Clé de Sécurité",
+    "Great! This Security Phrase looks strong enough.": "Super ! Cette phrase de passe a l’air assez solide.",
+    "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Nous avons stocké une copie chiffrée de vos clés sur notre serveur. Sécurisez vos sauvegardes avec une phrase de passe.",
+    "Use Security Key": "Utiliser la Clé de Sécurité",
+    "Use Security Key or Phrase": "Utilisez votre Clé de Sécurité ou phrase de passe",
+    "You have no visible notifications.": "Vous n’avez aucune notification visible.",
+    "Upgrade to pro": "Mettre à jour vers pro",
+    "Great, that'll help people know it's you": "Super, ceci aidera des personnes à confirmer qu’il s’agit bien de vous",
+    "<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 pour votre page de communauté</h1>\n<p>\n    Utilisez la description longue pour présenter la communauté aux nouveaux membres\n    ou pour diffuser des <a href=\"foo\">liens</a> importants\n</p>\n<p>\n    Vous pouvez même ajouter des images avec des URL Matrix <img src=\"mxc://url\" />\n</p>\n",
+    "Use email to optionally be discoverable by existing contacts.": "Utiliser une adresse e-mail pour pouvoir être découvert par des contacts existants.",
+    "Use email or phone to optionally be discoverable by existing contacts.": "Utiliser une adresse e-mail ou un numéro de téléphone pour pouvoir être découvert par des contacts existants.",
+    "Add an email to be able to reset your password.": "Ajouter une adresse e-mail pour pouvoir réinitialiser votre mot de passe.",
+    "Forgot password?": "Mot de passe oublié ?",
+    "That phone number doesn't look quite right, please check and try again": "Ce numéro de téléphone ne semble pas correct, merci de vérifier et réessayer",
+    "Enter email address": "Saisir l’adresse e-mail",
+    "Enter phone number": "Saisir le numéro de téléphone",
+    "Something went wrong in confirming your identity. Cancel and try again.": "Une erreur s’est produite lors de la vérification de votre identité. Annulez et réessayez.",
+    "Open the link in the email to continue registration.": "Ouvrez le lien dans l’e-mail pour poursuivre l’inscription.",
+    "A confirmation email has been sent to %(emailAddress)s": "Un e-mail de confirmation a été envoyé à %(emailAddress)s",
+    "Hold": "Mettre en pause",
+    "Resume": "Reprendre",
+    "If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Si vous avez oublié votre Clé de Sécurité, vous pouvez <button>définir de nouvelles options de récupération</button>",
+    "Access your secure message history and set up secure messaging by entering your Security Key.": "Accédez à votre historique de messages chiffrés et mettez en place la messagerie sécurisée en entrant votre Clé de Sécurité.",
+    "Not a valid Security Key": "Clé de Sécurité invalide",
+    "This looks like a valid Security Key!": "Ça ressemble à une Clé de Sécurité !",
+    "Enter Security Key": "Saisir la clé de sécurité",
+    "If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Si vous avez oublié votre phrase de passe vous pouvez <button1>utiliser votre Clé de Sécurité</button1> ou <button2>définir de nouvelles options de récupération</button2>",
+    "Access your secure message history and set up secure messaging by entering your Security Phrase.": "Accédez à votre historique de messages chiffrés et mettez en place la messagerie sécurisée en entrant votre phrase de passe.",
+    "Enter Security Phrase": "Saisir la phrase de passe",
+    "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "La sauvegarde n’a pas pu être déchiffrée avec cette phrase de passe : merci de vérifier que vous avez saisi la bonne phrase de passe.",
+    "Incorrect Security Phrase": "Phrase de passe incorrecte",
+    "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "La sauvegarde n’a pas pu être déchiffrée avec cette Clé de Sécurité : merci de vérifier que vous avez saisi la bonne Clé de Sécurité.",
+    "Security Key mismatch": "Pas de correspondance entre les Clés de Sécurité",
+    "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Impossible d’accéder à l’espace de stockage sécurisé. Merci de vérifier que vous avez saisi la bonne phrase de passe.",
+    "Invalid Security Key": "Clé de Sécurité invalide",
+    "Wrong Security Key": "Mauvaise Clé de Sécurité",
+    "Remember this": "Mémoriser ceci",
+    "The widget will verify your user ID, but won't be able to perform actions for you:": "Ce widget vérifiera votre identifiant d’utilisateur, mais ne pourra pas effectuer des actions en votre nom :",
+    "Allow this widget to verify your identity": "Autoriser ce widget à vérifier votre identité",
+    "Decline All": "Tout refuser",
+    "Approve": "Approuver",
+    "This widget would like to:": "Le widget voudrait :",
+    "Approve widget permissions": "Approuver les permissions du widget",
+    "We recommend you change your password and Security Key in Settings immediately": "Nous vous recommandons de changer votre mot de passe et Clé de Sécurité dans les Paramètres immédiatement",
+    "Minimize dialog": "Réduire la modale",
+    "Maximize dialog": "Maximiser la modale",
+    "%(hostSignupBrand)s Setup": "Configuration de %(hostSignupBrand)s",
+    "You should know": "Vous devriez connaître",
+    "Privacy Policy": "Politique de confidentialité",
+    "Cookie Policy": "Politique de gestion des cookies",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Consultez nos <privacyPolicyLink />, <termsOfServiceLink /> et <cookiePolicyLink />.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Continuer permettra temporairement au processus de configuration de %(hostSignupBrand)s d’accéder à votre compte pour récupérer les adresses email vérifiées. Les données ne sont pas stockées.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Impossible de vous connecter à votre serveur d’accueil. Merci de fermer cette modale et de réessayer.",
+    "Abort": "Annuler",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Êtes-vous sûr de vouloir annuler la création de cet hôte ? Le process ne pourra pas être repris.",
+    "Confirm abort of host creation": "Confirmer l’annulation de la création de cet hôte",
+    "There was an error finding this widget.": "Erreur lors de la récupération de ce widget.",
+    "Windows": "Windows",
+    "Screens": "Écrans",
+    "Share your screen": "Partager votre écran",
+    "Set my room layout for everyone": "Définir ma disposition de salon pour tout le monde",
+    "Open dial pad": "Ouvrir le pavé de numérotation",
+    "Start a Conversation": "Démarrer une conversation",
+    "Recently visited rooms": "Salons visités récemment",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Sauvegardez vos clés de chiffrement et les données de votre compte au casoù vous perdiez l’accès à vos sessions. Vos clés seront sécurisés avec une Clé de Sécurité unique.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Mettre en cache localement et de manière sécurisée les messages chiffrés pour qu’ils apparaissent dans les résultats de recherche, en utilisant %(size)s pour stocker les messages de %(rooms)s salons.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Mettre en cache localement et de manière sécurisée les messages chiffrés pour qu’ils apparaissent dans les résultats de recherche, en utilisant %(size)s pour stocker les messages de %(rooms)s salons.",
+    "Channel: <channelLink/>": "Canal : <channelLink/>",
+    "Workspace: <networkLink/>": "Espace de travail : <networkLink/>",
+    "Dial pad": "Pavé de numérotation",
+    "There was an error looking up the phone number": "Erreur lors de la recherche de votre numéro de téléphone",
+    "Unable to look up phone number": "Impossible de trouver votre numéro de téléphone",
+    "Use Ctrl + F to search": "Utilisez Control + F pour rechercher",
+    "Use Command + F to search": "Utilisez Commande + F pour rechercher",
+    "Show line numbers in code blocks": "Afficher les numéros de ligne dans les blocs de code",
+    "Expand code blocks by default": "Dérouler les blocs de code par défaut",
+    "Show stickers button": "Afficher le bouton stickers",
+    "Use app": "Utiliser l’application",
+    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web est expérimental sur téléphone. Pour une meilleure expérience et bénéficier des dernières fonctionnalités, utilisez notre application native gratuite.",
+    "Use app for a better experience": "Utilisez une application pour une meilleure expérience",
+    "See text messages posted to your active room": "Voir les messages textuels dans le salon actif",
+    "See text messages posted to this room": "Voir les messages textuels envoyés dans ce salon",
+    "Send text messages as you in your active room": "Envoyez des messages textuels en tant que vous-même dans le salon actif",
+    "Send text messages as you in this room": "Envoyez des messages textuels en tant que vous-même dans ce salon",
+    "See when the name changes in your active room": "Suivre les changements de nom dans le salon actif",
+    "Change which room, message, or user you're viewing": "Changer le salon, message, ou la personne que vous visualisez",
+    "Change which room you're viewing": "Changer le salon que vous visualisez",
+    "Remain on your screen while running": "Restez sur votre écran pendant l’exécution",
+    "%(senderName)s has updated the widget layout": "%(senderName)s a mis à jour la disposition du widget",
+    "Converts the DM to a room": "Transformer le message privé en salon",
+    "Converts the room to a DM": "Transformer le salon en message privé",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Votre serveur d’accueil a rejeté la demande de connexion. Ceci pourrait être dû à une connexion qui prend trop de temps. Si cela persiste, merci de contacter l’administrateur de votre serveur d’accueil.",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Votre serveur d’accueil n’est pas accessible, nous n’avons pas pu vous connecter. Merci de réessayer. Si cela persiste, merci de contacter l’administrateur de votre serveur d’accueil.",
+    "Try again": "Réessayez",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Nous avons demandé à votre navigateur de mémoriser votre serveur d’accueil, mais il semble l’avoir oublié. Rendez-vous à la page de connexion et réessayez.",
+    "We couldn't log you in": "Impossible de vous déconnecter"
 }

From a5cdc79f94047e31abc8d8ceb50a6d3ba12aa634 Mon Sep 17 00:00:00 2001
From: Maxime Corteel <maxime.corteel@harelsystems.com>
Date: Mon, 22 Feb 2021 12:18:39 +0000
Subject: [PATCH 126/389] Translated using Weblate (French)

Currently translated at 100.0% (2764 of 2764 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 | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index b98edea1b1..c942cae520 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -862,7 +862,7 @@
     "Upgrade this room to version %(version)s": "Mettre à niveau ce salon vers la version %(version)s",
     "Forces the current outbound group session in an encrypted room to be discarded": "Force la session de groupe sortante actuelle dans un salon chiffré à être rejetée",
     "Unable to connect to Homeserver. Retrying...": "Impossible de se connecter au serveur d'accueil. Reconnexion...",
-    "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s à défini l'adresse principale pour ce salon comme %(address)s.",
+    "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s a défini l'adresse principale pour ce salon comme %(address)s.",
     "%(senderName)s removed the main address for this room.": "%(senderName)s a supprimé l'adresse principale de ce salon.",
     "%(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 utilise maintenant 3 à 5 fois moins de mémoire, en ne chargeant les informations des autres utilisateurs que quand elles sont nécessaires. Veuillez patienter pendant que l'on se resynchronise avec le serveur !",
     "Updating %(brand)s": "Mise à jour de %(brand)s",
@@ -970,7 +970,7 @@
     "Invite anyway": "Inviter quand même",
     "Whether or not you're logged in (we don't record your username)": "Si vous êtes connecté ou pas (votre nom d'utilisateur n'est pas enregistré)",
     "Upgrades a room to a new version": "Met à niveau un salon vers une nouvelle version",
-    "Sets the room name": "Défini le nom du salon",
+    "Sets the room name": "Définit le nom du salon",
     "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s a mis à niveau ce salon.",
     "%(displayName)s is typing …": "%(displayName)s est en train d'écrire…",
     "%(names)s and %(count)s others are typing …|other": "%(names)s et %(count)s autres sont en train d'écrire…",
@@ -1085,7 +1085,7 @@
     "Recovery Method Removed": "Méthode de récupération supprimée",
     "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si vous n'avez pas supprimé la méthode de récupération, un attaquant peut être en train d'essayer d'accéder à votre compte. Modifiez le mot de passe de votre compte et configurez une nouvelle méthode de récupération dans les réglages.",
     "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Le fichier \"%(fileName)s\" dépasse la taille limite autorisée par ce serveur pour les téléchargements",
-    "Gets or sets the room topic": "Récupère ou défini le sujet du salon",
+    "Gets or sets the room topic": "Récupère ou définit le sujet du salon",
     "This room has no topic.": "Ce salon n'a pas de sujet.",
     "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s a rendu le salon public à tous ceux qui en connaissent le lien.",
     "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s a rendu le salon disponible sur invitation seulement.",
@@ -2894,7 +2894,7 @@
     "Homeserver": "Serveur d'accueil",
     "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.": "Vous pouvez utiliser les options de serveur personnalisés pour vous connecter à d'autres serveurs Matrix en spécifiant une URL de serveur d'accueil différente. Celà vous permet d'utiliser Element avec un compte Matrix existant sur un serveur d'accueil différent.",
     "Server Options": "Options serveur",
-    "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Les messages ici sont chiffrés de bout en bout. Quand les gens joignent, vous pouvez les vérifiez dans leur profil, tappez juste sur leur 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.": "Les messages ici sont chiffrés de bout en bout. Quand les gens joignent, vous pouvez les vérifier dans leur profil, tapez simplement sur leur avatar.",
     "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Les messages ici sont chiffrés de bout en bout. Vérifiez %(displayName)s dans leur profil - tapez sur leur avatar.",
     "Role": "Rôle",
     "Use the + to make a new room or explore existing ones below": "Utilisez le + pour créer un nouveau salon ou explorer les existantes ci-dessous",

From 23b16b62f205ae23b5234196970c3f629f412619 Mon Sep 17 00:00:00 2001
From: MusiCode1 <MusiCode3@gmail.com>
Date: Sun, 21 Feb 2021 20:59:15 +0000
Subject: [PATCH 127/389] Translated using Weblate (Hebrew)

Currently translated at 98.5% (2723 of 2764 strings)

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

diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json
index 30ca524699..1dd492767e 100644
--- a/src/i18n/strings/he.json
+++ b/src/i18n/strings/he.json
@@ -2774,5 +2774,16 @@
     "Workspace: <networkLink/>": "סביבת עבודה: <networkLink/>",
     "Use Ctrl + F to search": "השתמש ב- Ctrl + F כדי לחפש",
     "Use Command + F to search": "השתמש ב- Command + F כדי לחפש",
-    "Change which room, message, or user you're viewing": "שנה את החדר, ההודעה או המשתמש שאתה צופה בו"
+    "Change which room, message, or user you're viewing": "שנה את החדר, ההודעה או המשתמש שאתה צופה בו",
+    "Expand code blocks by default": "הרחב את בלוקי הקוד כברירת מחדל",
+    "Show stickers button": "הצג את לחצן הסטיקרים",
+    "Use app": "השתמש באפליקציה",
+    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web הוא ניסיוני במובייל. לקבלת חוויה טובה יותר והתכונות העדכניות ביותר, השתמש באפליקציה המקורית החינמית שלנו.",
+    "Use app for a better experience": "השתמש באפליקציה לחוויה טובה יותר",
+    "Converts the DM to a room": "המר את ה- DM לחדר שיחוח",
+    "Converts the room to a DM": "המר את החדר ל- DM",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "לא ניתן היה להגיע לשרת הבית שלך ולא היה ניתן להתחבר. נסה שוב. אם זה נמשך, אנא פנה למנהל שרת הבית שלך.",
+    "Try again": "נסה שוב",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "ביקשנו מהדפדפן לזכור באיזה שרת בית אתה משתמש כדי לאפשר לך להיכנס, אך למרבה הצער הדפדפן שלך שכח אותו. עבור לדף הכניסה ונסה שוב.",
+    "We couldn't log you in": "לא הצלחנו להתחבר אליך"
 }

From fb083d13ad6bafc3b804c7a75c30de16c558f6e8 Mon Sep 17 00:00:00 2001
From: Men770 <men770b@gmail.com>
Date: Thu, 11 Feb 2021 08:55:30 +0000
Subject: [PATCH 128/389] Translated using Weblate (Hebrew)

Currently translated at 98.5% (2723 of 2764 strings)

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

diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json
index 1dd492767e..dda9902e72 100644
--- a/src/i18n/strings/he.json
+++ b/src/i18n/strings/he.json
@@ -46,7 +46,7 @@
     "Unpin Message": "שחרר צימוד הודעה",
     "Online": "מקוון",
     "Register": "רשום",
-    "Rooms": "חדרי שיחוח",
+    "Rooms": "חדרים",
     "Add rooms to this community": "הוסף חדר שיחוח לקהילה זו",
     "OK": "בסדר",
     "Operation failed": "פעולה נכשלה",

From 44153f9cfb53747764f0a5a16e3c52577998becc Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Fri, 12 Feb 2021 14:45:14 +0000
Subject: [PATCH 129/389] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2764 of 2764 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 | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 20f39d61bc..398dd04ec2 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -3058,5 +3058,7 @@
     "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Tudjon meg többet innen: <privacyPolicyLink />, <termsOfServiceLink /> és <cookiePolicyLink />.",
     "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Folytatva %(hostSignupBrand)s beállítási folyamat ideiglenes hozzáférést kap a fiókadatok elérésére az ellenőrzött e-mail cím megszerzésének érdekében. Ezt az adat nincs elmenetve.",
     "Failed to connect to your homeserver. Please close this dialog and try again.": "A matrix szerverhez való csatlakozás nem sikerült. Zárja be ezt az ablakot és próbálja újra.",
-    "Abort": "Megszakítás"
+    "Abort": "Megszakítás",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Biztos benne, hogy meg kívánja szakítani a gazdagép létrehozásának a folyamatát? A folyamat nem folytatható.",
+    "Confirm abort of host creation": "Erősítse meg a gazdagép készítés megszakítását"
 }

From 376c675593531c4582d0c8da948e7fdb6ac56f78 Mon Sep 17 00:00:00 2001
From: jelv <post@jelv.nl>
Date: Mon, 15 Feb 2021 11:48:18 +0000
Subject: [PATCH 130/389] Translated using Weblate (Dutch)

Currently translated at 100.0% (2764 of 2764 strings)

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

diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 82b8cd39d5..101d997d9c 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -24,7 +24,7 @@
     "Banned users": "Verbannen gebruikers",
     "Bans user with given id": "Verbant de gebruiker met de gegeven ID",
     "Call Timeout": "Oproeptime-out",
-    "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>.": "Kan geen verbinding maken met de thuisserver via HTTP wanneer er een HTTPS-URL in uw browserbalk staat. Gebruik HTTPS of <a>schakel onveilige scripts in</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>.": "Kan geen verbinding maken met de homeserver via HTTP wanneer er een HTTPS-URL in uw browserbalk staat. Gebruik HTTPS of <a>schakel onveilige scripts in</a>.",
     "Change Password": "Wachtwoord veranderen",
     "%(senderName)s changed their profile picture.": "%(senderName)s heeft een nieuwe profielfoto ingesteld.",
     "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s heeft het machtsniveau van %(powerLevelDiffText)s gewijzigd.",
@@ -87,10 +87,10 @@
     "olm version:": "olm-versie:",
     "Password": "Wachtwoord",
     "Passwords can't be empty": "Wachtwoorden kunnen niet leeg zijn",
-    "Permissions": "Toestemmingen",
+    "Permissions": "Rechten",
     "Phone": "Telefoonnummer",
     "Private Chat": "Privégesprek",
-    "Privileged Users": "Bevoorrechte gebruikers",
+    "Privileged Users": "Bevoegde gebruikers",
     "Profile": "Profiel",
     "Public Chat": "Openbaar gesprek",
     "Reason": "Reden",
@@ -124,7 +124,7 @@
     "%(weekDayName)s %(time)s": "%(weekDayName)s, %(time)s",
     "Set a display name:": "Stel een weergavenaam in:",
     "Upload an avatar:": "Upload een avatar:",
-    "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.": "Geen verbinding met de thuisserver - controleer uw verbinding, zorg ervoor dat het <a>SSL-certificaat van de thuisserver</a> vertrouwd is en dat er geen browserextensies verzoeken blokkeren.",
+    "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.": "Geen verbinding met de homeserver - controleer uw verbinding, zorg ervoor dat het <a>SSL-certificaat van de homeserver</a> vertrouwd is en dat er geen browserextensies verzoeken blokkeren.",
     "Cryptography": "Cryptografie",
     "Current password": "Huidig wachtwoord",
     "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s heeft de gespreksnaam verwijderd.",
@@ -143,14 +143,14 @@
     "Deops user with given id": "Ontmachtigt gebruiker met de gegeven ID",
     "Default": "Standaard",
     "Displays action": "Toont actie",
-    "Emoji": "Emoticons",
+    "Emoji": "Emoji",
     "%(senderName)s ended the call.": "%(senderName)s heeft opgehangen.",
     "Enter passphrase": "Voer wachtwoord in",
     "Error decrypting attachment": "Fout bij het ontsleutelen van de bijlage",
     "Error: Problem communicating with the given homeserver.": "Fout: probleem bij communicatie met de gegeven thuisserver.",
     "Existing Call": "Bestaande oproep",
     "Export": "Wegschrijven",
-    "Export E2E room keys": "E2E-gesprekssleutels wegschrijven",
+    "Export E2E room keys": "E2E-gesprekssleutels exporteren",
     "Failed to ban user": "Verbannen van gebruiker is mislukt",
     "Failed to change power level": "Wijzigen van machtsniveau is mislukt",
     "Failed to fetch avatar URL": "Ophalen van avatar-URL is mislukt",
@@ -181,7 +181,7 @@
     "Identity Server is": "Identiteitsserver is",
     "I have verified my email address": "Ik heb mijn e-mailadres geverifieerd",
     "Import": "Inlezen",
-    "Import E2E room keys": "E2E-gesprekssleutels inlezen",
+    "Import E2E room keys": "E2E-gesprekssleutels importeren",
     "Incoming call from %(name)s": "Inkomende oproep van %(name)s",
     "Incoming video call from %(name)s": "Inkomende video-oproep van %(name)s",
     "Incoming voice call from %(name)s": "Inkomende spraakoproep van %(name)s",
@@ -202,7 +202,7 @@
     "Last seen": "Laatst gezien",
     "Leave room": "Gesprek verlaten",
     "%(targetName)s left the room.": "%(targetName)s heeft het gesprek verlaten.",
-    "Logout": "Afmelden",
+    "Logout": "Uitloggen",
     "Low priority": "Lage prioriteit",
     "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s heeft de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor alle gespreksleden, vanaf het moment dat ze uitgenodigd zijn.",
     "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s heeft de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor alle gespreksleden, vanaf het moment dat ze toegetreden zijn.",
@@ -222,7 +222,7 @@
     "Failed to kick": "Uit het gesprek zetten is mislukt",
     "%(senderName)s requested a VoIP conference.": "%(senderName)s heeft een VoIP-vergadering aangevraagd.",
     "Results from DuckDuckGo": "Resultaten van DuckDuckGo",
-    "Return to login screen": "Terug naar het aanmeldscherm",
+    "Return to login screen": "Terug naar het loginscherm",
     "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s heeft geen toestemming u meldingen te sturen - controleer uw browserinstellingen",
     "%(brand)s was not given permission to send notifications - please try again": "%(brand)s kreeg geen toestemming u meldingen te sturen - probeer het opnieuw",
     "%(brand)s version:": "%(brand)s-versie:",
@@ -230,7 +230,7 @@
     "Room Colour": "Gesprekskleur",
     "%(roomName)s does not exist.": "%(roomName)s bestaat niet.",
     "%(roomName)s is not accessible at this time.": "%(roomName)s is op dit moment niet toegankelijk.",
-    "Rooms": "Groepsgesprekken",
+    "Rooms": "Groepen",
     "Save": "Opslaan",
     "Search failed": "Zoeken mislukt",
     "Searches DuckDuckGo for results": "Zoekt op DuckDuckGo voor resultaten",
@@ -248,7 +248,7 @@
     "Kicks user with given id": "Stuurt de gebruiker met de gegeven ID uit het gesprek",
     "%(senderName)s set a profile picture.": "%(senderName)s heeft een profielfoto ingesteld.",
     "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s heeft %(displayName)s als weergavenaam aangenomen.",
-    "Show timestamps in 12 hour format (e.g. 2:30pm)": "Tijd in 12-uursformaat weergeven (bv. 2:30pm)",
+    "Show timestamps in 12 hour format (e.g. 2:30pm)": "Tijd in 12-uursformaat tonen (bv. 2:30pm)",
     "Signed Out": "Afgemeld",
     "Sign in": "Aanmelden",
     "Sign out": "Afmelden",
@@ -265,7 +265,7 @@
     "This phone number is already in use": "Dit telefoonnummer is al in gebruik",
     "This room": "Dit gesprek",
     "This room is not accessible by remote Matrix servers": "Dit gesprek is niet toegankelijk vanaf externe Matrix-servers",
-    "To use it, just wait for autocomplete results to load and tab through them.": "Om het te gebruiken, wacht u tot de automatisch aangevulde resultaten geladen zijn en tabt u erdoorheen.",
+    "To use it, just wait for autocomplete results to load and tab through them.": "Om het te gebruiken, wacht u tot de autoaanvullen resultaten geladen zijn en tabt u erdoorheen.",
     "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "U heeft gepoogd een gegeven punt in de tijdslijn van dit gesprek te laden, maar u bent niet bevoegd het desbetreffende bericht te zien.",
     "Tried to load a specific point in this room's timeline, but was unable to find it.": "Geprobeerd een gegeven punt in de tijdslijn van dit gesprek te laden, maar kon dit niet vinden.",
     "Unable to add email address": "Kan e-mailadres niet toevoegen",
@@ -312,8 +312,8 @@
     "You have no visible notifications": "U heeft geen zichtbare meldingen",
     "You must <a>register</a> to use this functionality": "U dient u te <a>registreren</a> om deze functie te gebruiken",
     "You need to be able to invite users to do that.": "Dit vereist de bevoegdheid gebruikers uit te nodigen.",
-    "You need to be logged in.": "Hiervoor dient u aangemeld te zijn.",
-    "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Zo te zien is uw e-mailadres op deze thuisserver niet aan een Matrix-ID gekoppeld.",
+    "You need to be logged in.": "Hiervoor dient u ingelogd te zijn.",
+    "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Zo te zien is uw e-mailadres op deze homeserver niet aan een Matrix-ID gekoppeld.",
     "You seem to be in a call, are you sure you want to quit?": "Het ziet er naar uit dat u in gesprek bent, weet u zeker dat u wilt afsluiten?",
     "You seem to be uploading files, are you sure you want to quit?": "Het ziet er naar uit dat u bestanden aan het uploaden bent, weet u zeker dat u wilt afsluiten?",
     "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "U zult deze veranderingen niet terug kunnen draaien, daar u de gebruiker tot uw eigen niveau promoveert.",
@@ -328,7 +328,7 @@
     "Active call": "Actieve oproep",
     "Please select the destination room for this message": "Selecteer het bestemmingsgesprek voor dit bericht",
     "New Password": "Nieuw wachtwoord",
-    "Start automatically after system login": "Automatisch starten na systeemaanmelding",
+    "Start automatically after system login": "Automatisch starten na systeemlogin",
     "Analytics": "Statistische gegevens",
     "Options": "Opties",
     "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s verzamelt anonieme analysegegevens die het mogelijk maken de toepassing te verbeteren.",
@@ -405,7 +405,7 @@
     "Create": "Aanmaken",
     "Featured Rooms:": "Prominente gesprekken:",
     "Featured Users:": "Prominente gebruikers:",
-    "Automatically replace plain text Emoji": "Tekst automatisch vervangen door emoticons",
+    "Automatically replace plain text Emoji": "Tekst automatisch vervangen door emoji",
     "Failed to upload image": "Uploaden van afbeelding is mislukt",
     "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s-widget toegevoegd door %(senderName)s",
     "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s-widget verwijderd door %(senderName)s",
@@ -440,7 +440,7 @@
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s %(day)s %(monthName)s %(fullYear)s",
     "Enable inline URL previews by default": "Inline URL-voorvertoning standaard inschakelen",
     "Enable URL previews for this room (only affects you)": "URL-voorvertoning in dit gesprek inschakelen (geldt alleen voor u)",
-    "Enable URL previews by default for participants in this room": "URL-voorvertoning standaard voor alle deelnemers aan dit gesprek inschakelen",
+    "Enable URL previews by default for participants in this room": "URL-voorvertoning voor alle deelnemers aan dit gesprek standaard inschakelen",
     "%(senderName)s sent an image": "%(senderName)s heeft een afbeelding gestuurd",
     "%(senderName)s sent a video": "%(senderName)s heeft een video gestuurd",
     "%(senderName)s uploaded a file": "%(senderName)s heeft een bestand geüpload",
@@ -484,10 +484,10 @@
     "'%(groupId)s' is not a valid community ID": "‘%(groupId)s’ is geen geldige gemeenschaps-ID",
     "Flair": "Badge",
     "Showing flair for these communities:": "Badges voor deze gemeenschappen weergeven:",
-    "This room is not showing flair for any communities": "Dit gesprek geeft geen badges voor gemeenschappen weer",
+    "This room is not showing flair for any communities": "Dit gesprek geeft geen gemeenschapsbadges weer",
     "New community ID (e.g. +foo:%(localDomain)s)": "Nieuwe gemeenschaps-ID (bv. +foo:%(localDomain)s)",
-    "URL previews are enabled by default for participants in this room.": "URL-voorvertoningen zijn voor leden van dit gesprek standaard ingeschakeld.",
-    "URL previews are disabled by default for participants in this room.": "URL-voorvertoningen zijn voor leden van dit gesprek standaard uitgeschakeld.",
+    "URL previews are enabled by default for participants in this room.": "URL-voorvertoningen zijn voor deelnemers van dit gesprek standaard ingeschakeld.",
+    "URL previews are disabled by default for participants in this room.": "URL-voorvertoningen zijn voor deelnemers van dit gesprek standaard uitgeschakeld.",
     "An email has been sent to %(emailAddress)s": "Er is een e-mail naar %(emailAddress)s verstuurd",
     "A text message has been sent to %(msisdn)s": "Er is een sms naar %(msisdn)s verstuurd",
     "Remove from community": "Verwijderen uit gemeenschap",
@@ -607,7 +607,7 @@
     "Community %(groupId)s not found": "Gemeenschap %(groupId)s is niet gevonden",
     "Failed to load %(groupId)s": "Laden van %(groupId)s is mislukt",
     "Old cryptography data detected": "Oude cryptografiegegevens gedetecteerd",
-    "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Er zijn gegevens van een oudere versie van %(brand)s gevonden, die problemen veroorzaakt hebben met de eind-tot-eind-versleuteling in de oude versie. Onlangs vanuit de oude versie verzonden eind-tot-eind-versleutelde berichten zijn mogelijk onontsleutelbaar in deze versie. Ook kunnen berichten die met deze versie uitgewisseld zijn falen. Mocht u problemen ervaren, meld u dan opnieuw aan. Schrijf uw sleutels weg en lees ze weer in om uw berichtgeschiedenis te behouden.",
+    "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Er zijn gegevens van een oudere versie van %(brand)s gevonden, die problemen veroorzaakt hebben met de eind-tot-eind-versleuteling in de oude versie. Onlangs vanuit de oude versie verzonden eind-tot-eind-versleutelde berichten zijn mogelijk onontsleutelbaar in deze versie. Ook kunnen berichten die met deze versie uitgewisseld zijn falen. Mocht u problemen ervaren, log dan opnieuw in. Exporteer uw sleutels en importeer ze weer om uw berichtgeschiedenis te behouden.",
     "Your Communities": "Uw gemeenschappen",
     "Error whilst fetching joined communities": "Er is een fout opgetreden bij het ophalen van de gemeenschappen waarvan u lid bent",
     "Create a new community": "Maak een nieuwe gemeenschap aan",
@@ -620,8 +620,8 @@
     "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is belangrijk voor ons, dus we verzamelen geen persoonlijke of identificeerbare gegevens voor onze gegevensanalyse.",
     "Learn more about how we use analytics.": "Lees meer over hoe we uw gegevens gebruiken.",
     "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Er is een e-mail naar %(emailAddress)s verstuurd. Klik hieronder van zodra u de koppeling erin hebt gevolgd.",
-    "Please note you are logging into the %(hs)s server, not matrix.org.": "Merk op dat u zich aanmeldt bij de %(hs)s-server, niet matrix.org.",
-    "This homeserver doesn't offer any login flows which are supported by this client.": "Deze thuisserver heeft geen aanmeldmethodes die door deze cliënt worden ondersteund.",
+    "Please note you are logging into the %(hs)s server, not matrix.org.": "Let op dat u inlogt bij de %(hs)s-server, niet matrix.org.",
+    "This homeserver doesn't offer any login flows which are supported by this client.": "Deze homeserver heeft geen loginmethodes die door deze cliënt worden ondersteund.",
     "Ignores a user, hiding their messages from you": "Negeert een gebruiker, waardoor de berichten ervan onzichtbaar voor u worden",
     "Stops ignoring a user, showing their messages going forward": "Stopt het negeren van een gebruiker, hierdoor worden de berichten van de gebruiker weer zichtbaar",
     "Notify the whole room": "Laat dit aan het hele groepsgesprek weten",
@@ -633,16 +633,16 @@
     "Your language of choice": "De door u gekozen taal",
     "Which officially provided instance you are using, if any": "Welke officieel aangeboden instantie u eventueel gebruikt",
     "Whether or not you're using the Richtext mode of the Rich Text Editor": "Of u de tekstverwerker al dan niet in de modus voor opgemaakte tekst gebruikt",
-    "Your homeserver's URL": "De URL van uw thuisserver",
+    "Your homeserver's URL": "De URL van uw homeserver",
     "<a>In reply to</a> <pill>": "<a>Als antwoord op</a> <pill>",
     "This room is not public. You will not be able to rejoin without an invite.": "Dit is geen openbaar gesprek. Slechts op uitnodiging zult u opnieuw kunnen toetreden.",
     "were unbanned %(count)s times|one": "zijn ontbannen",
     "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s heeft %(displayName)s als weergavenaam aangenomen.",
     "Key request sent.": "Sleutelverzoek verstuurd.",
-    "Did you know: you can use communities to filter your %(brand)s experience!": "Wist u dat: u gemeenschappen kunt gebruiken om uw %(brand)s-beleving te filteren!",
+    "Did you know: you can use communities to filter your %(brand)s experience!": "Tip: u kunt gemeenschappen gebruiken om uw %(brand)s-beleving te filteren!",
     "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Versleep een gemeenschapsavatar naar het filterpaneel helemaal links op het scherm om een filter in te stellen. Daarna kunt u op de avatar in het filterpaneel klikken wanneer u zich wilt beperken tot de gesprekken en mensen uit die gemeenschap.",
     "Clear filter": "Filter wissen",
-    "Failed to set direct chat tag": "Instellen van tweegesprekslabel is mislukt",
+    "Failed to set direct chat tag": "Instellen van direct gespreklabel is mislukt",
     "Failed to remove tag %(tagName)s from room": "Verwijderen van %(tagName)s-label van gesprek is mislukt",
     "Failed to add tag %(tagName)s to room": "Toevoegen van %(tagName)s-label aan gesprek is mislukt",
     "Stickerpack": "Stickerpakket",
@@ -658,7 +658,7 @@
     "Who can join this community?": "Wie kan er tot deze gemeenschap toetreden?",
     "Everyone": "Iedereen",
     "Leave this community": "Deze gemeenschap verlaten",
-    "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Voor het oplossen van in GitHub gemelde problemen helpen foutopsporingslogboeken ons enorm. Deze bevatten wel gebruiksgegevens (waaronder uw gebruikersnaam, de ID’s of bijnamen van de gesprekken en groepen die u heeft bezocht, en de namen van andere gebruikers), maar geen berichten.",
+    "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Voor het oplossen van, via GitHub, gemelde problemen helpen foutopsporingslogboeken ons enorm. Deze bevatten wel gebruiksgegevens (waaronder uw gebruikersnaam, de ID’s of bijnamen van de gesprekken en groepen die u heeft bezocht, en de namen van andere gebruikers), maar geen berichten.",
     "Submit debug logs": "Foutopsporingslogboeken indienen",
     "Opens the Developer Tools dialog": "Opent het dialoogvenster met ontwikkelaarsgereedschap",
     "Fetching third party location failed": "Het ophalen van de locatie van de derde partij is mislukt",
@@ -686,7 +686,7 @@
     "Resend": "Opnieuw versturen",
     "Error saving email notification preferences": "Fout bij het opslaan van de meldingsvoorkeuren voor e-mail",
     "Messages containing my display name": "Berichten die mijn weergavenaam bevatten",
-    "Messages in one-to-one chats": "Berichten in een-op-een chats",
+    "Messages in one-to-one chats": "Berichten in één-op-één gesprekken",
     "Unavailable": "Niet beschikbaar",
     "View Decrypted Source": "Ontsleutelde bron bekijken",
     "Failed to update keywords": "Bijwerken van trefwoorden is mislukt",
@@ -697,14 +697,14 @@
     "An error occurred whilst saving your email notification preferences.": "Er is een fout opgetreden tijdens het opslaan van uw e-mailmeldingsvoorkeuren.",
     "Explore Room State": "Gesprekstoestand verkennen",
     "Source URL": "Bron-URL",
-    "Messages sent by bot": "Berichten verzonden door een robot",
+    "Messages sent by bot": "Berichten verzonden door een bot",
     "Filter results": "Resultaten filteren",
     "Members": "Leden",
     "No update available.": "Geen update beschikbaar.",
     "Noisy": "Lawaaierig",
     "Collecting app version information": "App-versieinformatie wordt verzameld",
     "Keywords": "Trefwoorden",
-    "Enable notifications for this account": "Meldingen inschakelen voor deze account",
+    "Enable notifications for this account": "Meldingen voor dit account inschakelen",
     "Invite to this community": "Uitnodigen in deze gemeenschap",
     "Messages containing <span>keywords</span>": "Berichten die <span>trefwoorden</span> bevatten",
     "Room not found": "Gesprek niet gevonden",
@@ -730,7 +730,7 @@
     "Collecting logs": "Logboeken worden verzameld",
     "You must specify an event type!": "U dient een gebeurtenistype op te geven!",
     "(HTTP status %(httpStatus)s)": "(HTTP-status %(httpStatus)s)",
-    "Invite to this room": "Uitnodigen tot dit gesprek",
+    "Invite to this room": "Uitnodigen voor dit gesprek",
     "Send logs": "Logboeken versturen",
     "All messages": "Alle berichten",
     "Call invitation": "Oproep-uitnodiging",
@@ -750,7 +750,7 @@
     "Forward Message": "Bericht doorsturen",
     "Back": "Terug",
     "Reply": "Beantwoorden",
-    "Show message in desktop notification": "Bericht tonen in bureaubladmelding",
+    "Show message in desktop notification": "Bericht in bureaubladmelding tonen",
     "Unhide Preview": "Voorvertoning weergeven",
     "Unable to join network": "Kon niet toetreden tot dit netwerk",
     "Sorry, your browser is <b>not</b> able to run %(brand)s.": "Sorry, uw browser werkt <b>niet</b> met %(brand)s.",
@@ -788,12 +788,12 @@
     "Your device resolution": "De resolutie van uw apparaat",
     "Missing roomId.": "roomId ontbreekt.",
     "Always show encryption icons": "Versleutelingspictogrammen altijd tonen",
-    "Send analytics data": "Statistische gegevens (analytics) versturen",
+    "Send analytics data": "Statistische gegevens versturen",
     "Enable widget screenshots on supported widgets": "Widget-schermafbeeldingen inschakelen op ondersteunde widgets",
     "Muted Users": "Gedempte gebruikers",
     "Popout widget": "Widget in nieuw venster openen",
     "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Kan de gebeurtenis waarop gereageerd was niet laden. Wellicht bestaat die niet, of heeft u geen toestemming die te bekijken.",
-    "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Dit zal uw account voorgoed onbruikbaar maken. U zult zich niet meer kunnen aanmelden, en niemand anders zal zich met dezelfde gebruikers-ID kunnen registreren. Hierdoor zal uw account alle gesprekken waaraan ze deelneemt verlaten, en worden de accountgegevens verwijderd van de identiteitsserver. <b>Deze stap is onomkeerbaar.</b>",
+    "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Dit zal uw account voorgoed onbruikbaar maken. U zult niet meer kunnen inloggen, en niemand anders zal zich met dezelfde gebruikers-ID kunnen registreren. Hierdoor zal uw account alle gesprekken waaraan u deelneemt verlaten, en worden de accountgegevens verwijderd van de identiteitsserver. <b>Deze stap is onomkeerbaar.</b>",
     "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Het sluiten van uw account <b>maakt op zich niet dat wij de door u verstuurde berichten vergeten.</b> Als u wilt dat wij uw berichten vergeten, vink dan het vakje hieronder aan.",
     "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "De zichtbaarheid van berichten in Matrix is zoals bij e-mails. Het vergeten van uw berichten betekent dat berichten die u heeft verstuurd niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan.",
     "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Vergeet bij het sluiten van mijn account alle door mij verstuurde berichten (<b>Let op:</b> hierdoor zullen gebruikers een onvolledig beeld krijgen van gesprekken)",
@@ -805,9 +805,9 @@
     "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Het legen van de opslag van uw browser zal het probleem misschien verhelpen, maar zal u ook afmelden en uw gehele versleutelde gespreksgeschiedenis onleesbaar maken.",
     "Collapse Reply Thread": "Reactieketting dichtvouwen",
     "Can't leave Server Notices room": "Kan servermeldingsgesprek niet verlaten",
-    "This room is used for important messages from the Homeserver, so you cannot leave it.": "Dit gesprek is bedoeld voor belangrijke berichten van de thuisserver, dus u kunt het niet verlaten.",
+    "This room is used for important messages from the Homeserver, so you cannot leave it.": "Dit gesprek is bedoeld voor belangrijke berichten van de homeserver, dus u kunt het niet verlaten.",
     "Terms and Conditions": "Gebruiksvoorwaarden",
-    "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Om de %(homeserverDomain)s-thuisserver te blijven gebruiken, zult u de gebruiksvoorwaarden moeten bestuderen en aanvaarden.",
+    "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Om de %(homeserverDomain)s-homeserver te blijven gebruiken, zult u de gebruiksvoorwaarden moeten bestuderen en aanvaarden.",
     "Review terms and conditions": "Gebruiksvoorwaarden lezen",
     "Call in Progress": "Lopend gesprek",
     "A call is currently being placed!": "Er wordt al een oproep gemaakt!",
@@ -820,7 +820,7 @@
     "Share Link to User": "Koppeling naar gebruiker delen",
     "Share room": "Gesprek delen",
     "System Alerts": "Systeemmeldingen",
-    "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In versleutelde gesprekken zoals deze zijn URL-voorvertoningen standaard uitgeschakeld, om te voorkomen dat uw thuisserver (waar de voorvertoningen worden gemaakt) informatie kan verzamelen over de koppelingen die u hier ziet.",
+    "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In versleutelde gesprekken zoals deze zijn URL-voorvertoningen standaard uitgeschakeld, om te voorkomen dat uw homeserver (waar de voorvertoningen worden gemaakt) informatie kan verzamelen over de koppelingen die u hier ziet.",
     "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Als iemand een URL in een bericht invoegt, kan er een URL-voorvertoning weergegeven worden met meer informatie over de koppeling, zoals de titel, omschrijving en een afbeelding van de website.",
     "The email field must not be blank.": "Het e-mailveld mag niet leeg zijn.",
     "The phone number field must not be blank.": "Het telefoonnummerveld mag niet leeg zijn.",
@@ -840,20 +840,20 @@
     "Audio Output": "Geluidsuitgang",
     "Ignored users": "Genegeerde gebruikers",
     "Bulk options": "Bulkopties",
-    "This homeserver has hit its Monthly Active User limit.": "Deze thuisserver heeft zijn limiet voor maandelijks actieve gebruikers bereikt.",
-    "This homeserver has exceeded one of its resource limits.": "Deze thuisserver heeft één van zijn systeembronlimieten overschreden.",
-    "Whether or not you're logged in (we don't record your username)": "Of u al dan niet aangemeld bent (we slaan uw gebruikersnaam niet op)",
-    "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Het bestand ‘%(fileName)s’ is groter dan de uploadlimiet van de thuisserver",
+    "This homeserver has hit its Monthly Active User limit.": "Deze homeserver heeft zijn limiet voor maandelijks actieve gebruikers bereikt.",
+    "This homeserver has exceeded one of its resource limits.": "Deze homeserver heeft één van zijn systeembronlimieten overschreden.",
+    "Whether or not you're logged in (we don't record your username)": "Of u al dan niet ingelogd bent (we slaan uw gebruikersnaam niet op)",
+    "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Het bestand ‘%(fileName)s’ is groter dan de uploadlimiet van de homeserver",
     "Unable to load! Check your network connectivity and try again.": "Laden mislukt! Controleer uw netwerktoegang en probeer het nogmaals.",
     "Failed to invite users to the room:": "Kon de volgende gebruikers hier niet uitnodigen:",
     "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Plakt ¯\\_(ツ)_/¯ vóór een bericht zonder opmaak",
     "Upgrades a room to a new version": "Actualiseert het gesprek tot een nieuwe versie",
-    "Changes your display nickname in the current room only": "Stelt uw weergavenaam enkel in het huidige gesprek in",
+    "Changes your display nickname in the current room only": "Stelt uw weergavenaam alleen in het huidige gesprek in",
     "Gets or sets the room topic": "Verkrijgt het onderwerp van het gesprek of stelt het in",
     "This room has no topic.": "Dit gesprek heeft geen onderwerp.",
     "Sets the room name": "Stelt de gespreksnaam in",
     "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s heeft dit gesprek bijgewerkt.",
-    "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s heeft het gesprek toegankelijk gemaakt voor iedereen die de verwijzing kent.",
+    "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s heeft het gesprek toegankelijk gemaakt voor iedereen die de koppeling kent.",
     "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s heeft het gesprek enkel op uitnodiging toegankelijk gemaakt.",
     "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s heeft de toegangsregel veranderd naar ‘%(rule)s’",
     "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s heeft gasten toegestaan het gesprek te betreden.",
@@ -869,12 +869,12 @@
     "%(names)s and %(count)s others are typing …|one": "%(names)s en nog iemand zijn aan het typen…",
     "%(names)s and %(lastPerson)s are typing …": "%(names)s en %(lastPerson)s zijn aan het typen…",
     "Please <a>contact your service administrator</a> to continue using the service.": "Gelieve <a>contact op te nemen met uw systeembeheerder</a> om deze dienst te blijven gebruiken.",
-    "Unable to connect to Homeserver. Retrying...": "Kon geen verbinding met de thuisserver maken. Nieuwe poging…",
+    "Unable to connect to Homeserver. Retrying...": "Kon geen verbinding met de homeserver maken. Nieuwe poging…",
     "Unrecognised address": "Adres niet herkend",
     "You do not have permission to invite people to this room.": "U bent niet bevoegd anderen tot dit gesprek uit te nodigen.",
     "User %(userId)s is already in the room": "De gebruiker %(userId)s is al aanwezig",
-    "User %(user_id)s does not exist": "Er bestaat geen gebruiker ‘%(user_id)s’",
-    "User %(user_id)s may or may not exist": "Er bestaat mogelijk geen gebruiker ‘%(user_id)s’",
+    "User %(user_id)s does not exist": "Er bestaat geen gebruiker %(user_id)s",
+    "User %(user_id)s may or may not exist": "Er bestaat mogelijk geen gebruiker %(user_id)s",
     "The user must be unbanned before they can be invited.": "De gebruiker kan niet uitgenodigd worden voordat diens ban teniet is gedaan.",
     "Unknown server error": "Onbekende serverfout",
     "Use a few words, avoid common phrases": "Gebruik enkele woorden - maar geen bekende uitdrukkingen",
@@ -905,28 +905,28 @@
     "Straight rows of keys are easy to guess": "Zo’n aaneengesloten rijtje toetsen is eenvoudig te raden",
     "Short keyboard patterns are easy to guess": "Korte patronen op het toetsenbord worden gemakkelijk geraden",
     "There was an error joining the room": "Er is een fout opgetreden bij het betreden van het gesprek",
-    "Sorry, your homeserver is too old to participate in this room.": "Helaas - uw thuisserver is te oud voor dit gesprek.",
-    "Please contact your homeserver administrator.": "Gelieve contact op te nemen met de beheerder van uw thuisserver.",
+    "Sorry, your homeserver is too old to participate in this room.": "Helaas - uw homeserver is te oud voor dit gesprek.",
+    "Please contact your homeserver administrator.": "Gelieve contact op te nemen met de beheerder van uw homeserver.",
     "Custom user status messages": "Aangepaste gebruikersstatusberichten",
     "Group & filter rooms by custom tags (refresh to apply changes)": "Gesprekken groeperen en filteren volgens eigen labels (herlaad om de verandering te zien)",
     "Render simple counters in room header": "Eenvoudige tellers bovenaan het gesprek tonen",
     "Enable Emoji suggestions while typing": "Emoticons voorstellen tijdens het typen",
-    "Show a placeholder for removed messages": "Vulling tonen voor verwijderde berichten",
+    "Show a placeholder for removed messages": "Verwijderde berichten vulling tonen",
     "Show join/leave messages (invites/kicks/bans unaffected)": "Berichten over toe- en uittredingen tonen (dit heeft geen effect op uitnodigingen, berispingen of verbanningen)",
     "Show avatar changes": "Veranderingen van avatar tonen",
     "Show display name changes": "Veranderingen van weergavenamen tonen",
     "Show read receipts sent by other users": "Door andere gebruikers verstuurde leesbevestigingen tonen",
     "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Herinnering tonen om veilig berichtherstel in te schakelen in versleutelde gesprekken",
-    "Show avatars in user and room mentions": "Avatars tonen wanneer gebruikers of gesprekken vermeld worden",
-    "Enable big emoji in chat": "Grote emoticons in gesprekken inschakelen",
+    "Show avatars in user and room mentions": "Vermelde gebruikers- of gesprekkenavatars tonen",
+    "Enable big emoji in chat": "Grote emoji in gesprekken inschakelen",
     "Send typing notifications": "Typmeldingen versturen",
     "Enable Community Filter Panel": "Gemeenschapsfilterpaneel inschakelen",
-    "Allow Peer-to-Peer for 1:1 calls": "Peer-to-peer toestaan voor tweegesprekken",
-    "Prompt before sending invites to potentially invalid matrix IDs": "Bevestiging vragen voordat uitnodigingen naar mogelijk ongeldige Matrix-ID’s worden verstuurd",
+    "Allow Peer-to-Peer for 1:1 calls": "Peer-to-peer voor één-op-één oproepen toestaan",
+    "Prompt before sending invites to potentially invalid matrix IDs": "Uitnodigingen naar mogelijk ongeldige Matrix-ID’s bevestigen",
     "Show developer tools": "Ontwikkelgereedschap tonen",
     "Messages containing my username": "Berichten die mijn gebruikersnaam bevatten",
     "Messages containing @room": "Berichten die ‘@room’ bevatten",
-    "Encrypted messages in one-to-one chats": "Versleutelde berichten in een-op-een chats",
+    "Encrypted messages in one-to-one chats": "Versleutelde berichten in één-op-één gesprekken",
     "Encrypted messages in group chats": "Versleutelde berichten in groepsgesprekken",
     "The other party cancelled the verification.": "De tegenpartij heeft de verificatie geannuleerd.",
     "Verified!": "Geverifieerd!",
@@ -998,7 +998,7 @@
     "Anchor": "Anker",
     "Headphones": "Koptelefoon",
     "Folder": "Map",
-    "Pin": "Speld",
+    "Pin": "Vastmaken",
     "Yes": "Ja",
     "No": "Nee",
     "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "We hebben u een e-mail gestuurd om uw adres te verifiëren. Gelieve de daarin gegeven aanwijzingen op te volgen en dan op de knop hieronder te klikken.",
@@ -1007,7 +1007,7 @@
     "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Weet u het zeker? U zult uw versleutelde berichten verliezen als uw sleutels niet correct geback-upt zijn.",
     "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Versleutelde berichten zijn beveiligd met eind-tot-eind-versleuteling. Enkel de ontvanger(s) en u hebben de sleutels om deze berichten te lezen.",
     "Unable to load key backup status": "Kan sleutelback-upstatus niet laden",
-    "Restore from Backup": "Herstellen uit back-up",
+    "Restore from Backup": "Uit back-up herstellen",
     "Back up your keys before signing out to avoid losing them.": "Maak een back-up van uw sleutels vooraleer u zich afmeldt om ze niet te verliezen.",
     "Backing up %(sessionsRemaining)s keys...": "%(sessionsRemaining)s sleutels worden geback-upt…",
     "All keys backed up": "Alle sleutels zijn geback-upt",
@@ -1037,19 +1037,19 @@
     "For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "Klik <a>hier</a> voor hulp bij het gebruiken van %(brand)s, of begin een gesprek met onze robot met de knop hieronder.",
     "Help & About": "Hulp & Info",
     "Bug reporting": "Foutmeldingen",
-    "FAQ": "VGV",
+    "FAQ": "FAQ",
     "Versions": "Versies",
     "Preferences": "Instellingen",
     "Composer": "Opsteller",
     "Timeline": "Tijdslijn",
     "Room list": "Gesprekslijst",
-    "Autocomplete delay (ms)": "Vertraging voor automatisch aanvullen (ms)",
+    "Autocomplete delay (ms)": "Vertraging voor autoaanvullen (ms)",
     "Accept all %(invitedRooms)s invites": "Alle %(invitedRooms)s de uitnodigingen aannemen",
     "Key backup": "Sleutelback-up",
-    "Security & Privacy": "Veiligheid & privacy",
+    "Security & Privacy": "Veiligheid & Privacy",
     "Missing media permissions, click the button below to request.": "Mediatoestemmingen ontbreken, klik op de knop hieronder om deze aan te vragen.",
     "Request media permissions": "Mediatoestemmingen verzoeken",
-    "Voice & Video": "Spraak & video",
+    "Voice & Video": "Spraak & Video",
     "Room information": "Gespreksinformatie",
     "Internal room ID:": "Interne gespreks-ID:",
     "Room version": "Gespreksversie",
@@ -1061,7 +1061,7 @@
     "Change room name": "Gespreksnaam wijzigen",
     "Change main address for the room": "Hoofdadres voor het gesprek wijzigen",
     "Change history visibility": "Zichtbaarheid van geschiedenis wijzigen",
-    "Change permissions": "Toestemmingen wijzigen",
+    "Change permissions": "Rechten wijzigen",
     "Change topic": "Onderwerp wijzigen",
     "Modify widgets": "Widgets aanpassen",
     "Default role": "Standaardrol",
@@ -1073,8 +1073,8 @@
     "Remove messages": "Berichten verwijderen",
     "Notify everyone": "Iedereen melden",
     "Send %(eventType)s events": "%(eventType)s-gebeurtenissen versturen",
-    "Roles & Permissions": "Rollen & toestemmingen",
-    "Select the roles required to change various parts of the room": "Selecteer de rollen vereist om verschillende delen van het gesprek te wijzigen",
+    "Roles & Permissions": "Rollen & rechten",
+    "Select the roles required to change various parts of the room": "Selecteer de vereiste rollen om verschillende delen van het gesprek te wijzigen",
     "Enable encryption?": "Versleuteling inschakelen?",
     "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Gespreksversleuteling is onomkeerbaar. Berichten in versleutelde gesprekken zijn niet leesbaar voor de server; enkel voor de gespreksdeelnemers. Veel robots en overbruggingen werken niet correct in versleutelde gesprekken. <a>Lees meer over versleuteling.</a>",
     "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Wijzigingen aan wie de geschiedenis kan lezen gelden enkel voor toekomstige berichten in dit gesprek. De zichtbaarheid van de bestaande geschiedenis blijft ongewijzigd.",
@@ -1108,9 +1108,9 @@
     "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Kan geen profielen voor de Matrix-ID’s hieronder vinden - wilt u ze toch uitnodigen?",
     "Invite anyway and never warn me again": "Alsnog uitnodigen en mij nooit meer waarschuwen",
     "Invite anyway": "Alsnog uitnodigen",
-    "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Vooraleer u logboeken indient, dient u uw probleem te <a>melden op GitHub</a>.",
+    "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Voor u logboeken indient, dient u uw probleem te <a>melden op GitHub</a>.",
     "Unable to load commit detail: %(msg)s": "Kan commitdetail niet laden: %(msg)s",
-    "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Schrijf om uw gespreksgeschiedenis niet te verliezen vóór het afmelden uw gesprekssleutels weg. Dat moet vanuit de nieuwere versie van %(brand)s",
+    "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "Om uw gespreksgeschiedenis niet te verliezen vóór het uitloggen dient u uw veiligheidssleutel te exporteren. Dat moet vanuit de nieuwere versie van %(brand)s",
     "Incompatible Database": "Incompatibele database",
     "Continue With Encryption Disabled": "Verdergaan met versleuteling uitgeschakeld",
     "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verifieer deze gebruiker om hem/haar als vertrouwd te markeren. Gebruikers vertrouwen geeft u extra gemoedsrust bij het gebruik van eind-tot-eind-versleutelde berichten.",
@@ -1158,9 +1158,9 @@
     "Set status": "Status instellen",
     "Set a new status...": "Stel een nieuwe status in…",
     "Hide": "Verbergen",
-    "This homeserver would like to make sure you are not a robot.": "Deze thuisserver wil graag weten of u geen robot bent.",
-    "Please review and accept all of the homeserver's policies": "Gelieve het beleid van de thuisserver door te nemen en te aanvaarden",
-    "Please review and accept the policies of this homeserver:": "Gelieve het beleid van deze thuisserver door te nemen en te aanvaarden:",
+    "This homeserver would like to make sure you are not a robot.": "Deze homeserver wil graag weten of u geen robot bent.",
+    "Please review and accept all of the homeserver's policies": "Gelieve het beleid van de homeserver door te nemen en te aanvaarden",
+    "Please review and accept the policies of this homeserver:": "Gelieve het beleid van deze homeserver door te nemen en te aanvaarden:",
     "Your Modular server": "Uw Modular-server",
     "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Voer de locatie van uw Modular-thuisserver in. Deze kan uw eigen domeinnaam gebruiken, of een subdomein van <a>modular.im</a> zijn.",
     "Server Name": "Servernaam",
@@ -1186,9 +1186,9 @@
     "Couldn't load page": "Kon pagina niet laden",
     "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "U bent een beheerder van deze gemeenschap. U zult niet opnieuw kunnen toetreden zonder een uitnodiging van een andere beheerder.",
     "Want more than a community? <a>Get your own server</a>": "Wilt u meer dan een gemeenschap? <a>Verkrijg uw eigen server</a>",
-    "This homeserver does not support communities": "Deze thuisserver biedt geen ondersteuning voor gemeenschappen",
-    "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Uw bericht is niet verstuurd omdat deze thuisserver zijn limiet voor maandelijks actieve gebruikers heeft bereikt. Gelieve <a>contact op te nemen met uw dienstbeheerder</a> om de dienst te blijven gebruiken.",
-    "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Uw bericht is niet verstuurd omdat deze thuisserver een systeembronlimiet heeft overschreden. Gelieve <a>contact op te nemen met uw dienstbeheerder</a> om de dienst te blijven gebruiken.",
+    "This homeserver does not support communities": "Deze homeserver biedt geen ondersteuning voor gemeenschappen",
+    "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Uw bericht is niet verstuurd omdat deze homeserver zijn limiet voor maandelijks actieve gebruikers heeft bereikt. <a>Neem contact op met uw dienstbeheerder</a> om de dienst te blijven gebruiken.",
+    "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Uw bericht is niet verstuurd omdat deze homeserver een systeembronlimiet heeft overschreden. <a>Neem contact op met uw dienstbeheerder</a> om de dienst te blijven gebruiken.",
     "Guest": "Gast",
     "Could not load user profile": "Kon gebruikersprofiel niet laden",
     "Your Matrix account on %(serverName)s": "Uw Matrix-account op %(serverName)s",
@@ -1196,15 +1196,15 @@
     "Sign in instead": "Aanmelden",
     "Your password has been reset.": "Uw wachtwoord is opnieuw ingesteld.",
     "Set a new password": "Stel een nieuw wachtwoord in",
-    "Invalid homeserver discovery response": "Ongeldig thuisserverontdekkingsantwoord",
+    "Invalid homeserver discovery response": "Ongeldig homeserver-ontdekkingsantwoord",
     "Invalid identity server discovery response": "Ongeldig identiteitsserverontdekkingsantwoord",
     "General failure": "Algemene fout",
-    "This homeserver does not support login using email address.": "Deze thuisserver biedt geen ondersteuning voor aanmelden met e-mailadres.",
+    "This homeserver does not support login using email address.": "Deze homeserver biedt geen ondersteuning voor inloggen met e-mailadres.",
     "Please <a>contact your service administrator</a> to continue using this service.": "Gelieve <a>contact op te nemen met uw dienstbeheerder</a> om deze dienst te blijven gebruiken.",
-    "Failed to perform homeserver discovery": "Ontdekken van thuisserver is mislukt",
-    "Sign in with single sign-on": "Aanmelden met enkele aanmelding",
+    "Failed to perform homeserver discovery": "Ontdekken van homeserver is mislukt",
+    "Sign in with single sign-on": "Aanmelden met eenmalige aanmelding",
     "Create account": "Account aanmaken",
-    "Registration has been disabled on this homeserver.": "Registratie is uitgeschakeld op deze thuisserver.",
+    "Registration has been disabled on this homeserver.": "Registratie is uitgeschakeld op deze homeserver.",
     "Unable to query for supported registration methods.": "Kan ondersteunde registratiemethoden niet opvragen.",
     "Create your account": "Maak uw account aan",
     "Keep going...": "Doe verder…",
@@ -1239,7 +1239,7 @@
     "You cannot modify widgets in this room.": "U kunt de widgets in dit gesprek niet aanpassen.",
     "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s heeft de uitnodiging aan %(targetDisplayName)s toe te treden tot het gesprek ingetrokken.",
     "Upgrade this room to the recommended room version": "Werk dit gesprek bij tot de aanbevolen versie",
-    "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "Dit gesprek draait op groepsgespreksversie <roomVersion />, die door deze thuisserver als <i>onstabiel</i> is gemarkeerd.",
+    "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "Dit gesprek draait op groepsgespreksversie <roomVersion />, die door deze homeserver als <i>onstabiel</i> is gemarkeerd.",
     "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Bijwerken zal de huidige versie van dit gesprek sluiten, en onder dezelfde naam een bijgewerkte versie starten.",
     "Failed to revoke invite": "Intrekken van uitnodiging is mislukt",
     "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Kon de uitnodiging niet intrekken. De server ondervindt mogelijk een tijdelijk probleem, of u heeft niet het recht de uitnodiging in te trekken.",
@@ -1250,9 +1250,9 @@
     "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.": "Een widget op %(widgetUrl)s wil uw identiteit nagaan. Staat u dit toe, dan zal de widget wel uw gebruikers-ID kunnen nagaan, maar niet als u kunnen handelen.",
     "Remember my selection for this widget": "Onthoud mijn keuze voor deze widget",
     "Deny": "Weigeren",
-    "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s kon de protocollijst niet ophalen van de thuisserver. Mogelijk is de thuisserver te oud om derdepartijnetwerken te ondersteunen.",
+    "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s kon de protocollijst niet ophalen van de homeserver. Mogelijk is de homeserver te oud om derde-partij-netwerken te ondersteunen.",
     "%(brand)s failed to get the public room list.": "%(brand)s kon de lijst met openbare gesprekken niet verkrijgen.",
-    "The homeserver may be unavailable or overloaded.": "De thuisserver is mogelijk onbereikbaar of overbelast.",
+    "The homeserver may be unavailable or overloaded.": "De homeserver is mogelijk onbereikbaar of overbelast.",
     "You have %(count)s unread notifications in a prior version of this room.|other": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.",
     "You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.",
     "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u de icoontjes voor recente gesprekken (boven de gesprekkenlijst) al dan niet gebruikt",
@@ -1285,9 +1285,9 @@
     "Unbans user with given ID": "Ontbant de gebruiker met de gegeven ID",
     "Sends the given message coloured as a rainbow": "Verstuurt het gegeven bericht in regenboogkleuren",
     "Sends the given emote coloured as a rainbow": "Verstuurt de gegeven emoticon in regenboogkleuren",
-    "No homeserver URL provided": "Geen thuisserver-URL opgegeven",
-    "Unexpected error resolving homeserver configuration": "Onverwachte fout bij het controleren van de thuisserverconfiguratie",
-    "The user's homeserver does not support the version of the room.": "De thuisserver van de gebruiker biedt geen ondersteuning voor de gespreksversie.",
+    "No homeserver URL provided": "Geen homeserver-URL opgegeven",
+    "Unexpected error resolving homeserver configuration": "Onverwachte fout bij het controleren van de homeserverconfiguratie",
+    "The user's homeserver does not support the version of the room.": "De homeserver van de gebruiker biedt geen ondersteuning voor de gespreksversie.",
     "Show hidden events in timeline": "Verborgen gebeurtenissen op de tijdslijn weergeven",
     "When rooms are upgraded": "Wanneer gesprekken bijgewerkt worden",
     "this room": "dit gesprek",
@@ -1326,14 +1326,14 @@
     "Unable to validate homeserver/identity server": "Kan thuis-/identiteitsserver niet valideren",
     "Sign in to your Matrix account on <underlinedServerName />": "Meld u met uw Matrix-account op <underlinedServerName /> aan",
     "Use an email address to recover your account": "Gebruik een e-mailadres om uw account te herstellen",
-    "Enter email address (required on this homeserver)": "Voer een e-mailadres in (vereist op deze thuisserver)",
+    "Enter email address (required on this homeserver)": "Voer een e-mailadres in (vereist op deze homeserver)",
     "Doesn't look like a valid email address": "Dit lijkt geen geldig e-mailadres",
     "Enter password": "Voer wachtwoord in",
     "Password is allowed, but unsafe": "Wachtwoord is toegestaan, maar onveilig",
     "Nice, strong password!": "Dit is een sterk wachtwoord!",
     "Passwords don't match": "Wachtwoorden komen niet overeen",
     "Other users can invite you to rooms using your contact details": "Andere gebruikers kunnen u in gesprekken uitnodigen op basis van uw contactgegevens",
-    "Enter phone number (required on this homeserver)": "Voer telefoonnummer in (vereist op deze thuisserver)",
+    "Enter phone number (required on this homeserver)": "Voer telefoonnummer in (vereist op deze homeserver)",
     "Doesn't look like a valid phone number": "Dit lijkt geen geldig telefoonnummer",
     "Enter username": "Voer gebruikersnaam in",
     "Some characters not allowed": "Sommige tekens zijn niet toegestaan",
@@ -1343,7 +1343,7 @@
     "Your Matrix account on <underlinedServerName />": "Uw Matrix-account op <underlinedServerName />",
     "Failed to get autodiscovery configuration from server": "Ophalen van auto-ontdekkingsconfiguratie van server is mislukt",
     "Invalid base_url for m.homeserver": "Ongeldige base_url voor m.homeserver",
-    "Homeserver URL does not appear to be a valid Matrix homeserver": "De thuisserver-URL lijkt geen geldige Matrix-thuisserver",
+    "Homeserver URL does not appear to be a valid Matrix homeserver": "De homeserver-URL lijkt geen geldige Matrix-homeserver",
     "Invalid base_url for m.identity_server": "Ongeldige base_url voor m.identity_server",
     "Identity server URL does not appear to be a valid identity server": "De identiteitsserver-URL lijkt geen geldige identiteitsserver",
     "Low bandwidth mode": "Lagebandbreedtemodus",
@@ -1353,7 +1353,7 @@
     "Reset": "Opnieuw instellen",
     "Set a new custom sound": "Stel een nieuw aangepast geluid in",
     "Browse": "Bladeren",
-    "Cannot reach homeserver": "Kan thuisserver niet bereiken",
+    "Cannot reach homeserver": "Kan homeserver niet bereiken",
     "Ensure you have a stable internet connection, or get in touch with the server admin": "Zorg dat u een stabiele internetverbinding heeft, of neem contact op met de systeembeheerder",
     "Your %(brand)s is misconfigured": "Uw %(brand)s is onjuist geconfigureerd",
     "Ask your %(brand)s admin to check <a>your config</a> for incorrect or duplicate entries.": "Vraag uw %(brand)s-beheerder <a>uw configuratie</a> na te kijken op onjuiste of dubbele items.",
@@ -1362,12 +1362,12 @@
     "Cannot reach identity server": "Kan identiteitsserver niet bereiken",
     "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "U kunt zich registreren, maar sommige functies zullen pas beschikbaar zijn wanneer de identiteitsserver weer online is. Als u deze waarschuwing blijft zien, controleer dan uw configuratie of neem contact op met een serverbeheerder.",
     "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "U kunt uw wachtwoord opnieuw instellen, maar sommige functies zullen pas beschikbaar komen wanneer de identiteitsserver weer online is. Als u deze waarschuwing blijft zien, controleer dan uw configuratie of neem contact op met een serverbeheerder.",
-    "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "U kunt zich aanmelden, maar sommige functies zullen pas beschikbaar zijn wanneer de identiteitsserver weer online is. Als u deze waarschuwing blijft zien, controleer dan uw configuratie of neem contact op met een systeembeheerder.",
-    "<a>Log in</a> to your new account.": "<a>Meld u aan</a> met uw nieuwe account.",
-    "You can now close this window or <a>log in</a> to your new account.": "U kunt dit venster nu sluiten, of u met uw nieuwe account <a>aanmelden</a>.",
+    "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "U kunt inloggen, maar sommige functies zullen pas beschikbaar zijn wanneer de identiteitsserver weer online is. Als u deze waarschuwing blijft zien, controleer dan uw configuratie of neem contact op met een systeembeheerder.",
+    "<a>Log in</a> to your new account.": "<a>Login</a> met uw nieuwe account.",
+    "You can now close this window or <a>log in</a> to your new account.": "U kunt dit venster nu sluiten, of <a>login</a> met uw nieuwe account.",
     "Registration Successful": "Registratie geslaagd",
     "Upload all": "Alles versturen",
-    "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Uw nieuwe account (%(newAccountId)s) is geregistreerd, maar u bent reeds aangemeld met een andere account (%(loggedInUserId)s).",
+    "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Uw nieuwe account (%(newAccountId)s) is geregistreerd, maar u bent al ingelogd met een andere account (%(loggedInUserId)s).",
     "Continue with previous account": "Doorgaan met vorige account",
     "Loading room preview": "Gespreksweergave wordt geladen",
     "Edited at %(date)s. Click to view edits.": "Bewerkt op %(date)s. Klik om de bewerkingen te bekijken.",
@@ -1382,11 +1382,11 @@
     "Changes your avatar in all rooms": "Verandert uw avatar in alle gesprekken",
     "Removing…": "Bezig met verwijderen…",
     "Clear all data": "Alle gegevens wissen",
-    "Your homeserver doesn't seem to support this feature.": "Uw thuisserver biedt geen ondersteuning voor deze functie.",
+    "Your homeserver doesn't seem to support this feature.": "Uw homeserver biedt geen ondersteuning voor deze functie.",
     "Resend edit": "Bewerking opnieuw versturen",
     "Resend %(unsentCount)s reaction(s)": "%(unsentCount)s reactie(s) opnieuw versturen",
     "Resend removal": "Verwijdering opnieuw versturen",
-    "Failed to re-authenticate due to a homeserver problem": "Opnieuw aanmelden is mislukt wegens een probleem met de thuisserver",
+    "Failed to re-authenticate due to a homeserver problem": "Opnieuw aanmelden is mislukt wegens een probleem met de homeserver",
     "Failed to re-authenticate": "Opnieuw aanmelden is mislukt",
     "Enter your password to sign in and regain access to your account.": "Voer uw wachtwoord in om u aan te melden en toegang tot uw account te herkrijgen.",
     "Forgotten your password?": "Wachtwoord vergeten?",
@@ -1401,7 +1401,7 @@
     "Service": "Dienst",
     "Summary": "Samenvatting",
     "Sign in and regain access to your account.": "Meld u aan en herkrijg toegang tot uw account.",
-    "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt zich niet aanmelden met uw account. Neem voor meer informatie contact op met de beheerder van uw thuisserver.",
+    "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt zich niet aanmelden met uw account. Neem voor meer informatie contact op met de beheerder van uw homeserver.",
     "This account has been deactivated.": "Deze account is gesloten.",
     "Messages": "Berichten",
     "Actions": "Acties",
@@ -1433,7 +1433,7 @@
     "Command Help": "Hulp bij opdrachten",
     "No identity server is configured: add one in server settings to reset your password.": "Er is geen identiteitsserver geconfigureerd: voeg er één toe in de serverinstellingen om uw wachtwoord opnieuw in te stellen.",
     "Call failed due to misconfigured server": "Oproep mislukt door verkeerd geconfigureerde server",
-    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Vraag uw thuisserverbeheerder (<code>%(homeserverDomain)s</code>) een TURN-server te configureren teneinde oproepen betrouwbaar te doen werken.",
+    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Vraag uw homeserverbeheerder (<code>%(homeserverDomain)s</code>) een TURN-server te configureren voor de betrouwbaarheid van de oproepen.",
     "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.": "U kunt ook de publieke server op <code>turn.matrix.org</code> gebruiken, maar dit zal minder betrouwbaar zijn, en zal uw IP-adres met die server delen. U kunt dit ook beheren in de Instellingen.",
     "Try using turn.matrix.org": "Probeer turn.matrix.org te gebruiken",
     "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Sta de terugvalserver voor oproepbijstand turn.matrix.org toe wanneer uw homeserver er geen aanbiedt (uw IP-adres wordt gedeeld gedurende een oproep)",
@@ -1461,7 +1461,7 @@
     "You are still <b>sharing your personal data</b> on the identity server <idserver />.": "U deelt nog steeds <b>uw persoonlijke gegevens</b> op de identiteitsserver <idserver />.",
     "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We raden u aan uw e-mailadressen en telefoonnummers van de identiteitsserver te verwijderen vooraleer u de verbinding verbreekt.",
     "Disconnect anyway": "Verbinding toch verbreken",
-    "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Mocht u om bekenden te zoeken en zelf vindbaar te zijn <server /> niet willen gebruiken, voer dan hieronder een andere identiteitsserver in.",
+    "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Mocht u, om bekenden te zoeken en zelf vindbaar te zijn, <server /> niet willen gebruiken, voer dan hieronder een andere identiteitsserver in.",
     "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Een identiteitsserver is niet verplicht, maar zonder identiteitsserver zult u geen bekenden op e-mailadres of telefoonnummer kunnen zoeken, noch door hen vindbaar zijn.",
     "Do not use an identity server": "Geen identiteitsserver gebruiken",
     "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Aanvaard de gebruiksvoorwaarden van de identiteitsserver (%(serverName)s) om vindbaar te zijn op e-mailadres of telefoonnummer.",
@@ -1482,7 +1482,7 @@
     "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Bij een groot aantal berichten kan dit even duren. Herlaad uw cliënt niet gedurende deze tijd.",
     "Remove %(count)s messages|other": "%(count)s berichten verwijderen",
     "Deactivate user?": "Gebruiker deactiveren?",
-    "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?": "Deze gebruiker deactiveren zal hem/haar afmelden en verhinderen dat hij/zij zich weer aanmeldt. Bovendien zal hij/zij alle gesprekken waaraan hij/zij deelneemt verlaten. Deze actie is onherroepelijk. Weet u zeker dat u deze gebruiker wilt deactiveren?",
+    "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?": "Deze gebruiker deactiveren zal deze gebruiker uitloggen en verhinderen dat de gebruiker weer inlogt. Bovendien zal de gebruiker alle gesprekken waaraan de gebruiker deelneemt verlaten. Deze actie is onherroepelijk. Weet u zeker dat u deze gebruiker wilt deactiveren?",
     "Deactivate user": "Gebruiker deactiveren",
     "Remove recent messages": "Recente berichten verwijderen",
     "Bold": "Vet",
@@ -1498,8 +1498,8 @@
     "Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Gebruik een identiteitsserver om uit te nodigen op e-mailadres. <default>Gebruik de standaardserver (%(defaultIdentityServerName)s)</default> of beheer de server in de <settings>Instellingen</settings>.",
     "Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Gebruik een identiteitsserver om anderen uit te nodigen via e-mail. Beheer de server in de <settings>Instellingen</settings>.",
     "Please fill why you're reporting.": "Gelieve aan te geven waarom u deze melding indient.",
-    "Report Content to Your Homeserver Administrator": "Inhoud melden aan de beheerder van uw thuisserver",
-    "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.": "Dit bericht melden zal zijn unieke ‘gebeurtenis-ID’ versturen naar de beheerder van uw thuisserver. Als de berichten in dit gesprek versleuteld zijn, zal de beheerder van uw thuisserver het bericht niet kunnen lezen, noch enige bestanden of afbeeldingen zien.",
+    "Report Content to Your Homeserver Administrator": "Inhoud melden aan de beheerder van uw homeserver",
+    "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.": "Dit bericht melden zal zijn unieke ‘gebeurtenis-ID’ versturen naar de beheerder van uw homeserver. Als de berichten in dit gesprek versleuteld zijn, zal de beheerder van uw homeserver het bericht niet kunnen lezen, noch enige bestanden of afbeeldingen zien.",
     "Send report": "Rapport versturen",
     "Report Content": "Inhoud melden",
     "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Stel een e-mailadres voor accountherstel in. Gebruik eventueel een e-mailadres of telefoonnummer om vindbaar te zijn voor bestaande contacten.",
@@ -1515,7 +1515,7 @@
     "Find a room… (e.g. %(exampleRoom)s)": "Zoek een gesprek… (bv. %(exampleRoom)s)",
     "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Als u het gesprek dat u zoekt niet kunt vinden, vraag dan een uitnodiging, of <a>Maak een nieuw gesprek aan</a>.",
     "Explore rooms": "Gesprekken ontdekken",
-    "Show previews/thumbnails for images": "Toon miniaturen voor afbeeldingen",
+    "Show previews/thumbnails for images": "Miniaturen voor afbeeldingen tonen",
     "Clear cache and reload": "Cache wissen en herladen",
     "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "U staat op het punt 1 bericht door %(user)s te verwijderen. Dit is onherroepelijk. Wilt u doorgaan?",
     "Remove %(count)s messages|one": "1 bericht verwijderen",
@@ -1537,12 +1537,12 @@
     "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Verhinder gebruikers op andere Matrix-thuisservers de toegang tot dit gesprek (Deze instelling kan later niet meer aangepast worden!)",
     "To continue you need to accept the terms of this service.": "Om door te gaan dient u de dienstvoorwaarden te aanvaarden.",
     "Document": "Document",
-    "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Publieke sleutel van captcha ontbreekt in thuisserverconfiguratie. Gelieve dit te melden aan de beheerder van uw thuisserver.",
-    "Community Autocomplete": "Gemeenschappen automatisch aanvullen",
-    "Emoji Autocomplete": "Emoji’s automatisch aanvullen",
-    "Notification Autocomplete": "Meldingen automatisch voltooien",
-    "Room Autocomplete": "Gesprekken automatisch aanvullen",
-    "User Autocomplete": "Gebruikers automatisch aanvullen",
+    "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Publieke sleutel van captcha ontbreekt in homeserverconfiguratie. Meld dit aan de beheerder van uw homeserver.",
+    "Community Autocomplete": "Gemeenschappen autoaanvullen",
+    "Emoji Autocomplete": "Emoji autoaanvullen",
+    "Notification Autocomplete": "Meldingen autoaanvullen",
+    "Room Autocomplete": "Gesprekken autoaanvullen",
+    "User Autocomplete": "Gebruikers autoaanvullen",
     "Add Email Address": "E-mailadres toevoegen",
     "Add Phone Number": "Telefoonnummer toevoegen",
     "Your email address hasn't been verified yet": "Uw e-mailadres is nog niet geverifieerd",
@@ -1605,9 +1605,9 @@
     "Try out new ways to ignore people (experimental)": "Nieuwe manieren om mensen te negeren uitproberen (nog in ontwikkeling)",
     "Show info about bridges in room settings": "Toon bruginformatie in gespreksinstellingen",
     "Match system theme": "Aanpassen aan systeemthema",
-    "Never send encrypted messages to unverified sessions from this session": "Zend vanaf deze sessie nooit versleutelde berichten naar ongeverifieerde sessies",
-    "Never send encrypted messages to unverified sessions in this room from this session": "Zend vanaf deze sessie nooit versleutelde berichten naar ongeverifieerde sessies in dit gesprek",
-    "Enable message search in encrypted rooms": "Sta zoeken in versleutelde gesprekken toe",
+    "Never send encrypted messages to unverified sessions from this session": "Vanaf deze sessie nooit versleutelde berichten naar ongeverifieerde sessies versturen",
+    "Never send encrypted messages to unverified sessions in this room from this session": "Vanaf deze sessie nooit versleutelde berichten naar ongeverifieerde sessies in dit gesprek versturen",
+    "Enable message search in encrypted rooms": "Zoeken in versleutelde gesprekken inschakelen",
     "How fast should messages be downloaded.": "Ophaalfrequentie van berichten.",
     "My Ban List": "Mijn banlijst",
     "This is your list of users/servers you have blocked - don't leave the room!": "Dit is de lijst van door u geblokkeerde servers/gebruikers. Verlaat dit gesprek niet!",
@@ -1632,7 +1632,7 @@
     "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Momenteel stelt een wachtwoordswijziging alle berichtsleutels in alle sessies opnieuw in, en maakt zo oude versleutelde berichten onleesbaar - tenzij u uw sleutels eerst wegschrijft, en na afloop weer inleest. Dit zal verbeterd worden.",
     "in memory": "in het geheugen",
     "not found": "niet gevonden",
-    "Your homeserver does not support session management.": "Uw thuisserver ondersteunt geen sessiebeheer.",
+    "Your homeserver does not support session management.": "Uw homeserver ondersteunt geen sessiebeheer.",
     "Unable to load session list": "Kan sessielijst niet laden",
     "Delete %(count)s sessions|other": "%(count)s sessies verwijderen",
     "Delete %(count)s sessions|one": "%(count)s sessie verwijderen",
@@ -1653,10 +1653,10 @@
     "Enable": "Inschakelen",
     "Connecting to integration manager...": "Verbinding maken met de integratiebeheerder…",
     "Cannot connect to integration manager": "Kan geen verbinding maken met de integratiebeheerder",
-    "The integration manager is offline or it cannot reach your homeserver.": "De integratiebeheerder is offline of kan uw thuisserver niet bereiken.",
+    "The integration manager is offline or it cannot reach your homeserver.": "De integratiebeheerder is offline of kan uw homeserver niet bereiken.",
     "This session is backing up your keys. ": "Deze sessie maakt back-ups van uw sleutels. ",
     "not stored": "niet opgeslagen",
-    "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Uw wachtwoord is gewijzigd. U zult geen pushmeldingen op uw andere sessies meer ontvangen, totdat u zichzelf daarop opnieuw aanmeldt",
+    "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Uw wachtwoord is gewijzigd. U zult geen pushmeldingen op uw andere sessies meer ontvangen, totdat u zichzelf daar opnieuw inlogt",
     "Ignored/Blocked": "Genegeerd/geblokkeerd",
     "Error adding ignored user/server": "Fout bij het toevoegen van een genegeerde gebruiker/server",
     "Something went wrong. Please try again or view your console for hints.": "Er is iets fout gegaan. Probeer het opnieuw of bekijk de console om voor meer informatie.",
@@ -1681,8 +1681,8 @@
     "Subscribing to a ban list will cause you to join it!": "Wanneer u zich abonneert op een banlijst zal u eraan worden toegevoegd!",
     "If this isn't what you want, please use a different tool to ignore users.": "Als u dit niet wilt kunt u een andere methode gebruiken om gebruikers te negeren.",
     "Subscribe": "Abonneren",
-    "Enable desktop notifications for this session": "Bureaubladmeldingen inschakelen voor deze sessie",
-    "Enable audible notifications for this session": "Meldingen met geluid inschakelen voor deze sessie",
+    "Enable desktop notifications for this session": "Bureaubladmeldingen voor deze sessie inschakelen",
+    "Enable audible notifications for this session": "Meldingen met geluid voor deze sessie inschakelen",
     "You should:": "U zou best:",
     "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "uw browserextensies bekijken voor extensies die mogelijk de identiteitsserver blokkeren (zoals Privacy Badger)",
     "contact the administrators of identity server <idserver />": "contact opnemen met de beheerders van de identiteitsserver <idserver />",
@@ -1711,7 +1711,7 @@
     "Mod": "Mod",
     "rooms.": "gesprekken.",
     "Recent rooms": "Actuele gesprekken",
-    "Direct Messages": "Directe Berichten",
+    "Direct Messages": "Direct gesprek",
     "If disabled, messages from encrypted rooms won't appear in search results.": "Dit moet aan staan om te kunnen zoeken in versleutelde gesprekken.",
     "Indexed rooms:": "Geïndexeerde gesprekken:",
     "Cross-signing and secret storage are enabled.": "Kruiselings ondertekenen en sleutelopslag zijn ingeschakeld.",
@@ -1745,9 +1745,9 @@
     "Your keys are <b>not being backed up from this session</b>.": "Uw sleutels worden <b>niet geback-upt van deze sessie</b>.",
     "Clear notifications": "Meldingen wissen",
     "You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "U moet uw <b>persoonlijke informatie</b> van de identiteitsserver <idserver /> <b>verwijderen</b> voordat u zich ontkoppelt. Helaas kan de identiteitsserver <idserver /> op dit moment niet worden bereikt. Mogelijk is hij offline.",
-    "Your homeserver does not support cross-signing.": "Uw thuisserver biedt geen ondersteuning voor kruiselings ondertekenen.",
-    "Homeserver feature support:": "Functies ondersteund door thuisserver:",
-    "exists": "bestaat",
+    "Your homeserver does not support cross-signing.": "Uw homeserver biedt geen ondersteuning voor kruiselings ondertekenen.",
+    "Homeserver feature support:": "Homeserver ondersteund deze functies:",
+    "exists": "aanwezig",
     "Sign In or Create Account": "Meld u aan of maak een account aan",
     "Use your account or create a new one to continue.": "Gebruik uw bestaande account of maak een nieuwe aan om verder te gaan.",
     "Create Account": "Account aanmaken",
@@ -1794,8 +1794,8 @@
     "For extra security, verify this user by checking a one-time code on both of your devices.": "Als extra beveiliging kunt u deze gebruiker verifiëren door een eenmalige code op uw toestellen te controleren.",
     "Your messages are not secure": "Uw berichten zijn niet veilig",
     "One of the following may be compromised:": "Eén van volgende onderdelen kan gecompromitteerd zijn:",
-    "Your homeserver": "Uw thuisserver",
-    "The homeserver the user you’re verifying is connected to": "De thuisserver waarmee de gebruiker die u tracht te verifiëren verbonden is",
+    "Your homeserver": "Uw homeserver",
+    "The homeserver the user you’re verifying is connected to": "De homeserver waarmee de gebruiker die u probeert te verifiëren verbonden is",
     "Yours, or the other users’ internet connection": "De internetverbinding van uzelf of de andere gebruiker",
     "Yours, or the other users’ session": "De sessie van uzelf of de andere gebruiker",
     "Not Trusted": "Niet vertrouwd",
@@ -1810,7 +1810,7 @@
     "%(count)s sessions|other": "%(count)s sessies",
     "%(count)s sessions|one": "%(count)s sessie",
     "Hide sessions": "Sessies verbergen",
-    "Direct message": "Tweegesprek",
+    "Direct message": "Direct gesprek",
     "<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s",
     "This client does not support end-to-end encryption.": "Deze cliënt biedt geen ondersteuning voor eind-tot-eind-versleuteling.",
     "Messages in this room are not end-to-end encrypted.": "De berichten in dit gesprek worden niet eind-tot-eind-versleuteld.",
@@ -1846,7 +1846,7 @@
     "Reactions": "Reacties",
     "<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> heeft gereageerd met %(content)s</reactedWith>",
     "Frequently Used": "Vaak gebruikt",
-    "Smileys & People": "Emoticons en personen",
+    "Smileys & People": "Smileys & mensen",
     "Animals & Nature": "Dieren en natuur",
     "Food & Drink": "Eten en drinken",
     "Activities": "Activiteiten",
@@ -1875,7 +1875,7 @@
     "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Het verwijderen van sleutels voor kruiselings ondertekenen is onherroepelijk. Iedereen waarmee u geverifieerd heeft zal beveiligingswaarschuwingen te zien krijgen. U wilt dit hoogstwaarschijnlijk niet doen, tenzij u alle apparaten heeft verloren waarmee u kruiselings kon ondertekenen.",
     "Clear cross-signing keys": "Sleutels voor kruiselings ondertekenen wissen",
     "Clear all data in this session?": "Alle gegevens in deze sessie verwijderen?",
-    "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Het verwijderen van alle gegevens in deze sessie is onherroepelijk. Versleutelde berichten zullen verloren gaan, tenzij u een back-up van hun sleutels heeft.",
+    "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Het verwijderen van alle gegevens in deze sessie is onherroepelijk. Versleutelde berichten zullen verloren gaan, tenzij u een back-up van de sleutels heeft.",
     "Verify session": "Sessie verifiëren",
     "Session name": "Sessienaam",
     "Session key": "Sessiesleutel",
@@ -1888,7 +1888,7 @@
     "Integrations not allowed": "Integraties niet toegestaan",
     "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "Uw %(brand)s laat u geen integratiebeheerder gebruiken om dit te doen. Neem contact op met een beheerder.",
     "Failed to invite the following users to chat: %(csvUsers)s": "Het uitnodigen van volgende gebruikers voor gesprek is mislukt: %(csvUsers)s",
-    "We couldn't create your DM. Please check the users you want to invite and try again.": "Uw tweegesprek kon niet aangemaakt worden. Controleer de gebruikers die u wilt uitnodigen en probeer het opnieuw.",
+    "We couldn't create your DM. Please check the users you want to invite and try again.": "Uw direct gesprek kon niet aangemaakt worden. Controleer de gebruikers die u wilt uitnodigen en probeer het opnieuw.",
     "Something went wrong trying to invite the users.": "Er is een fout opgetreden bij het uitnodigen van de gebruikers.",
     "We couldn't invite those users. Please check the users you want to invite and try again.": "Deze gebruikers konden niet uitgenodigd worden. Controleer de gebruikers die u wilt uitnodigen en probeer het opnieuw.",
     "Failed to find the following users": "Kon volgende gebruikers niet vinden",
@@ -1937,10 +1937,10 @@
     "Without completing security on this session, it won’t have access to encrypted messages.": "Als u de beveiliging van deze sessie niet vervolledigt, zal ze geen toegang hebben tot uw versleutelde berichten.",
     "Go Back": "Terugkeren",
     "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.": "Door uw wachtwoord te wijzigen stelt u alle eind-tot-eind-versleutelingssleutels op al uw sessies opnieuw in, waardoor uw versleutelde gespreksgeschiedenis onleesbaar wordt. Stel sleutelback-up in of schrijf uw gesprekssleutels van een andere sessie weg vooraleer u een nieuw wachtwoord instelt.",
-    "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "U bent afgemeld bij al uw sessies en zult geen pushberichten meer ontvangen. Meld u op elk apparaat opnieuw aan om meldingen opnieuw in te schakelen.",
+    "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "U bent uitgelogd bij al uw sessies en zult geen pushberichten meer ontvangen. Meld u op elk apparaat opnieuw aan om meldingen opnieuw in te schakelen.",
     "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Herwin toegang tot uw account en herstel de tijdens deze sessie opgeslagen versleutelingssleutels, zonder welke sommige van uw beveiligde berichten in al uw sessies onleesbaar zijn.",
     "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Let op: uw persoonlijke gegevens (waaronder versleutelingssleutels) zijn nog steeds opgeslagen in deze sessie. Wis ze wanneer u klaar bent met deze sessie, of wanneer u zich wilt aanmelden met een andere account.",
-    "Command Autocomplete": "Opdrachten automatisch aanvullen",
+    "Command Autocomplete": "Opdrachten autoaanvullen",
     "DuckDuckGo Results": "DuckDuckGo-resultaten",
     "Enter your account password to confirm the upgrade:": "Voer uw accountwachtwoord in om het bijwerken te bevestigen:",
     "Restore your key backup to upgrade your encryption": "Herstel uw sleutelback-up om uw versleuteling bij te werken",
@@ -1955,8 +1955,8 @@
     "Your recovery key is in your <b>Downloads</b> folder.": "Uw herstelsleutel bevindt zich in uw <b>Downloads</b>-map.",
     "Upgrade your encryption": "Werk uw versleuteling bij",
     "Make a copy of your recovery key": "Maak een kopie van uw herstelsleutel",
-    "Unable to set up secret storage": "Kan geheime opslag niet instellen",
-    "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Zonder veilig berichtherstel in te stellen zult u uw versleutelde berichtgeschiedenis niet kunnen herstellen als u zich afmeldt of een andere sessie gebruikt.",
+    "Unable to set up secret storage": "Kan sleutelopslag niet instellen",
+    "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Zonder veilig berichtherstel in te stellen zult u uw versleutelde berichtgeschiedenis niet kunnen herstellen als u uitlogt of een andere sessie gebruikt.",
     "Create key backup": "Sleutelback-up aanmaken",
     "This session is encrypting history using the new recovery method.": "Deze sessie versleutelt uw geschiedenis aan de hand van de nieuwe herstelmethode.",
     "This session has detected that your recovery passphrase and key for Secure Messages have been removed.": "Deze sessie heeft gedetecteerd dat uw herstelwachtwoord en -sleutel voor beveiligde berichten verwijderd zijn.",
@@ -1970,19 +1970,19 @@
     "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Bekijk eerst het <a>beveiligingsopenbaarmakingsbeleid</a> van Matrix.org als u een probleem met de beveiliging van Matrix wilt melden.",
     "Not currently indexing messages for any room.": "Er worden momenteel voor geen enkel gesprek berichten geïndexeerd.",
     "%(doneRooms)s out of %(totalRooms)s": "%(doneRooms)s van %(totalRooms)s",
-    "Where you’re logged in": "Waar u aangemeld bent",
+    "Where you’re logged in": "Waar u ingelogd bent",
     "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Beheer hieronder de namen van uw sessies en meld ze af. <a>Of verifieer ze in uw gebruikersprofiel</a>.",
-    "Use Single Sign On to continue": "Ga verder met Eenmalige Aanmelding",
-    "Confirm adding this email address by using Single Sign On to prove your identity.": "Bevestig uw identiteit met Eenmalige Aanmelding om dit emailadres toe te voegen.",
-    "Single Sign On": "Eenmalige Aanmelding",
+    "Use Single Sign On to continue": "Ga verder met eenmalige aanmelding",
+    "Confirm adding this email address by using Single Sign On to prove your identity.": "Bevestig uw identiteit met uw eenmalige aanmelding om dit e-mailadres toe te voegen.",
+    "Single Sign On": "Eenmalige aanmelding",
     "Confirm adding email": "Bevestig toevoegen van het e-mailadres",
     "Click the button below to confirm adding this email address.": "Klik op de knop hieronder om dit e-mailadres toe te voegen.",
-    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bevestig uw identiteit met Eenmalige Aanmelding om dit telefoonnummer toe te voegen.",
+    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bevestig uw identiteit met uw eenmalige aanmelding om dit telefoonnummer toe te voegen.",
     "Confirm adding phone number": "Bevestig toevoegen van het telefoonnummer",
     "Click the button below to confirm adding this phone number.": "Klik op de knop hieronder om het toevoegen van dit telefoonnummer te bevestigen.",
     "If you cancel now, you won't complete your operation.": "Als u de operatie afbreekt kunt u haar niet voltooien.",
-    "Review where you’re logged in": "Kijk na waar u aangemeld bent",
-    "New login. Was this you?": "Nieuwe aanmelding - was u dat?",
+    "Review where you’re logged in": "Kijk na waar u ingelogd bent",
+    "New login. Was this you?": "Nieuwe login - was u dat?",
     "%(name)s is requesting verification": "%(name)s verzoekt om verificatie",
     "Sends a message as html, without interpreting it as markdown": "Stuurt een bericht als HTML, zonder markdown toe te passen",
     "Failed to set topic": "Kon onderwerp niet instellen",
@@ -2003,12 +2003,12 @@
     "Manually Verify by Text": "Handmatig middels een tekst",
     "Interactively verify by Emoji": "Interactief middels emojis",
     "Support adding custom themes": "Sta maatwerkthema's toe",
-    "Opens chat with the given user": "Start een tweegesprek met die gebruiker",
+    "Opens chat with the given user": "Start een gesprek met die gebruiker",
     "Sends a message to the given user": "Zendt die gebruiker een bericht",
     "Font scaling": "Lettergrootte",
     "Verify all your sessions to ensure your account & messages are safe": "Controleer al uw sessies om zeker te zijn dat uw account & berichten veilig zijn",
-    "Verify the new login accessing your account: %(name)s": "Verifieer de nieuwe aanmelding op uw account: %(name)s",
-    "Confirm your account deactivation by using Single Sign On to prove your identity.": "Bevestig uw intentie deze account te sluiten door met Single Sign On uw identiteit te bewijzen.",
+    "Verify the new login accessing your account: %(name)s": "Verifieer de nieuwe login op uw account: %(name)s",
+    "Confirm your account deactivation by using Single Sign On to prove your identity.": "Bevestig de deactivering van uw account door gebruik te maken van eenmalige aanmelding om uw identiteit te bewijzen.",
     "Are you sure you want to deactivate your account? This is irreversible.": "Weet u zeker dat u uw account wil sluiten? Dit is onomkeerbaar.",
     "Confirm account deactivation": "Bevestig accountsluiting",
     "Room name or address": "Gespreksnaam of -adres",
@@ -2017,8 +2017,8 @@
     "Help us improve %(brand)s": "Help ons %(brand)s nog beter te maken",
     "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Stuur <UsageDataLink>anonieme gebruiksinformatie</UsageDataLink> waarmee we %(brand)s kunnen verbeteren. Dit plaatst een <PolicyLink>cookie</PolicyLink>.",
     "I want to help": "Ik wil helpen",
-    "Your homeserver has exceeded its user limit.": "Uw thuisserver heeft het maximaal aantal gebruikers overschreden.",
-    "Your homeserver has exceeded one of its resource limits.": "Uw thuisserver heeft een van zijn limieten overschreden.",
+    "Your homeserver has exceeded its user limit.": "Uw homeserver heeft het maximaal aantal gebruikers overschreden.",
+    "Your homeserver has exceeded one of its resource limits.": "Uw homeserver heeft een van zijn limieten overschreden.",
     "Ok": "Oké",
     "Light": "Helder",
     "Dark": "Donker",
@@ -2312,20 +2312,20 @@
     "Are you sure you want to cancel entering passphrase?": "Weet u zeker, dat u het invoeren van uw wachtwoord wilt afbreken?",
     "Vatican City": "Vaticaanstad",
     "Taiwan": "Taiwan",
-    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Uw thuisserver wees uw aanmeldingspoging af. Dit kan zijn doordat het te lang heeft geduurd. Probeer het opnieuw. Als dit probleem zich blijft voordoen, neem contact op met de beheerder van uw thuisserver.",
-    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Uw thuisserver was onbereikbaar en kon u niet aanmelden, probeer het opnieuw. Wanneer dit probleem zich blijft voordoen, neem contact op met de beheerder van uw thuisserver.",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Uw homeserver wees uw inlogpoging af. Dit kan zijn doordat het te lang heeft geduurd. Probeer het opnieuw. Als dit probleem zich blijft voordoen, neem contact op met de beheerder van uw homeserver.",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Uw homeserver was onbereikbaar en kon u niet inloggen, probeer het opnieuw. Wanneer dit probleem zich blijft voordoen, neem contact op met de beheerder van uw homeserver.",
     "Try again": "Probeer opnieuw",
-    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "De browser is verzocht uw thuisserver te onthouden die u gebruikt om zich aan te melden, maar is deze vergeten. Ga naar de aanmeldpagina en probeer het opnieuw.",
-    "We couldn't log you in": "We konden u niet aanmelden",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "De browser is verzocht uw homeserver te onthouden die u gebruikt om zich aan te melden, maar is deze vergeten. Ga naar de aanmeldpagina en probeer het opnieuw.",
+    "We couldn't log you in": "We konden u niet inloggen",
     "Room Info": "Gespreksinfo",
-    "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is de grootste openbare thuisserver van de wereld, dus het is een goede plek voor vele.",
-    "Explore Public Rooms": "Verken openbare groepsgesprekken",
+    "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is de grootste openbare homeserver van de wereld, dus het is een goede plek voor vele.",
+    "Explore Public Rooms": "Verken openbare groepen",
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Privégesprekken zijn alleen zichtbaar en toegankelijk met een uitnodiging. Openbare gesprekken zijn zichtbaar en toegankelijk voor iedereen in deze gemeenschap.",
     "This room is public": "Dit gesprek is openbaar",
-    "Show previews of messages": "Toon voorvertoning van berichten",
+    "Show previews of messages": "Voorvertoning van berichten inschakelen",
     "Show message previews for reactions in all rooms": "Toon berichtvoorbeelden voor reacties in alle gesprekken",
-    "Explore public rooms": "Verken openbare groepsgesprekken",
-    "Leave Room": "Verlaat gesprek",
+    "Explore public rooms": "Verken openbare groepen",
+    "Leave Room": "Gesprek verlaten",
     "Room options": "Gesprekopties",
     "Start a conversation with someone using their name, email address or username (like <userId/>).": "Start een gesprek met iemand door hun naam, emailadres of gebruikersnaam (zoals <userId/>) te typen.",
     "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Berichten hier zijn eind-tot-eind versleuteld. Verifieer %(displayName)s op hun profiel - klik op hun avatar.",
@@ -2349,7 +2349,7 @@
     "Show rooms with unread messages first": "Gesprekken met ongelezen berichten als eerste tonen",
     "%(count)s results|one": "%(count)s resultaten",
     "%(count)s results|other": "%(count)s resultaten",
-    "Explore all public rooms": "Verken alle openbare groepsgesprekken",
+    "Explore all public rooms": "Verken alle openbare groepen",
     "Start a new chat": "Een nieuw gesprek beginnen",
     "Can't see what you’re looking for?": "Niet kunnen vinden waar u naar zocht?",
     "Custom Tag": "Aangepast label",
@@ -2359,11 +2359,11 @@
     "Hide Widgets": "Widgets verbergen",
     "This is the start of <roomName/>.": "Dit is het begin van <roomName/>.",
     "%(displayName)s created this room.": "%(displayName)s heeft dit gesprek aangemaakt.",
-    "You created this room.": "U heeft dit gesprek aangemaakt.",
+    "You created this room.": "U heeft dit gesprek gemaakt.",
     "Topic: %(topic)s ": "Onderwerp: %(topic)s ",
     "Topic: %(topic)s (<a>edit</a>)": "Onderwerp: %(topic)s (<a>bewerken</a>)",
-    "This is the beginning of your direct message history with <displayName/>.": "Dit is het begin van de geschiedenis van uw tweegesprek met <displayName/>.",
-    "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "De beheerder van uw server heeft eind-tot-eindversleuteling standaard uitgeschakeld in alle privégesprekken en tweegesprekken.",
+    "This is the beginning of your direct message history with <displayName/>.": "Dit is het begin van de geschiedenis van uw direct gesprek met <displayName/>.",
+    "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "De beheerder van uw server heeft eind-tot-eind-versleuteling standaard uitgeschakeld in alle privégesprekken en directe gesprekken.",
     "Scroll to most recent messages": "Spring naar meest recente bericht",
     "The authenticity of this encrypted message can't be guaranteed on this device.": "De echtheid van dit versleutelde bericht kan op dit apparaat niet worden gegarandeerd.",
     "To link to this room, please add an address.": "Voeg een adres toe om naar deze kamer te verwijzen.",
@@ -2392,10 +2392,10 @@
     "Click the button below to confirm deleting these sessions.|one": "Bevestig het verwijderen van deze sessie door op de knop hieronder te drukken.",
     "Click the button below to confirm deleting these sessions.|other": "Bevestig het verwijderen van deze sessies door op de knop hieronder te drukken.",
     "Confirm deleting these sessions": "Bevestig dat u deze sessies wilt verwijderen",
-    "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Bevestig uw identiteit met Eenmalige Aanmelding om deze sessies te verwijderen.",
-    "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Bevestig uw identiteit met Eenmalige Aanmelding om deze sessies te verwijderen.",
+    "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Bevestig uw identiteit met uw eenmalige aanmelding om deze sessies te verwijderen.",
+    "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Bevestig uw identiteit met uw eenmalige aanmelding om deze sessies te verwijderen.",
     "not found locally": "lokaal niet gevonden",
-    "cached locally": "Lokaal opgeslagen",
+    "cached locally": "lokaal opgeslagen",
     "not found in storage": "Niet gevonden in de opslag",
     "Channel: <channelLink/>": "Kanaal: <channelLink/>",
     "From %(deviceName)s (%(deviceId)s)": "Van %(deviceName)s %(deviceId)s",
@@ -2414,16 +2414,16 @@
     "Voice Call": "Spraakoproep",
     "Video Call": "Video-oproep",
     "sends snowfall": "Stuur sneeuwvlokken",
-    "sends confetti": "Stuur confetti",
+    "sends confetti": "verstuurt confetti",
     "sends fireworks": "Stuur vuurwerk",
     "Downloading logs": "Logboeken downloaden",
     "Uploading logs": "Logboeken versturen",
     "Use Ctrl + Enter to send a message": "Gebruik Ctrl + Enter om een bericht te sturen",
     "Use Command + Enter to send a message": "Gebruik Command (⌘) + Enter om een bericht te sturen",
-    "Use Ctrl + F to search": "Gebruik Ctrl + F om te zoeken",
-    "Use Command + F to search": "Gebruik Command (⌘) + F om te zoeken",
-    "Use a more compact ‘Modern’ layout": "Gebruik een meer compacte 'Moderne' indeling",
-    "Use custom size": "Gebruik aangepaste grootte",
+    "Use Ctrl + F to search": "Ctrl + F om te zoeken gebruiken",
+    "Use Command + F to search": "Command (⌘) + F om te zoeken gebruiken",
+    "Use a more compact ‘Modern’ layout": "Compacte 'Modern'-layout inschakelen",
+    "Use custom size": "Aangepaste lettergrootte gebruiken",
     "Font size": "Lettergrootte",
     "Enable advanced debugging for the room list": "Geavanceerde foutopsporing voor de gesprekkenlijst inschakelen",
     "Render LaTeX maths in messages": "Weergeef LaTeX-wiskundenotatie in berichten",
@@ -2478,8 +2478,8 @@
     "Looks good": "Ziet er goed uit",
     "Enter a server name": "Geef een servernaam",
     "Continue with %(provider)s": "Doorgaan met %(provider)s",
-    "Homeserver": "Thuisserver",
-    "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.": "U kunt de aangepaste serverinstellingen gebruiken om u aan te melden bij andere Matrix-servers, door een andere thuisserver-URL in te voeren. Dit laat u toe Element te gebruiken met een bestaande Matrix-account bij een andere thuisserver.",
+    "Homeserver": "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.": "U kunt de aangepaste serverinstellingen gebruiken om u aan te melden bij andere Matrix-servers, door een andere homeserver-URL in te voeren. Dit laat u toe Element te gebruiken met een bestaande Matrix-account bij een andere homeserver.",
     "Server Options": "Serverinstellingen",
     "This address is already in use": "Dit adres is al in gebruik",
     "This address is available to use": "Dit adres kan worden gebruikt",
@@ -2492,7 +2492,7 @@
     "Use the <a>Desktop app</a> to search encrypted messages": "Gebruik de <a>Desktop-toepassing</a> om alle versleutelde berichten te zien",
     "Categories": "Categorieën",
     "Can't load this message": "Dit bericht kan niet geladen worden",
-    "Click to view edits": "Druk om wijzigingen te weergeven",
+    "Click to view edits": "Klik om bewerkingen te zien",
     "Edited at %(date)s": "Bewerkt op %(date)s",
     "Message deleted on %(date)s": "Bericht verwijderd op %(date)s",
     "Message deleted by %(name)s": "Bericht verwijderd door %(name)s",
@@ -2517,7 +2517,7 @@
     "You can only pin up to %(count)s widgets|other": "U kunt maar %(count)s widgets vastzetten",
     "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "In versleutelde gesprekken zijn uw berichten beveiligd, enkel de ontvanger en u hebben de unieke sleutels om ze te ontsleutelen.",
     "Waiting for you to accept on your other session…": "Wachten totdat u uw uitnodiging in uw andere sessie aanneemt…",
-    "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Stel een adres in zodat gebruikers dit gesprek via uw thuisserver (%(localDomain)s) kunnen vinden",
+    "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Stel een adres in zodat gebruikers dit gesprek via uw homeserver (%(localDomain)s) kunnen vinden",
     "Local Addresses": "Lokale adressen",
     "Local address": "Lokaal adres",
     "The server has denied your request.": "De server heeft uw verzoek afgewezen.",
@@ -2549,9 +2549,9 @@
     "Manually verify all remote sessions": "Handmatig alle externe sessies verifiëren",
     "System font name": "Systeemlettertypenaam",
     "Use a system font": "Gebruik een systeemlettertype",
-    "Show line numbers in code blocks": "Toon regelnummers in codeblokken",
+    "Show line numbers in code blocks": "Regelnummers in codeblokken tonen",
     "Expand code blocks by default": "Standaard codeblokken uitvouwen",
-    "Show stickers button": "Toon stickers-knop",
+    "Show stickers button": "Stickers-knop tonen",
     "Offline encrypted messaging using dehydrated devices": "Offline versleutelde berichten met gebruik van uitgedroogde apparaten",
     "Show message previews for reactions in DMs": "Toon berichtvoorbeelden voor reacties in DM's",
     "New spinner design": "Nieuw laadicoonontwerp",
@@ -2595,7 +2595,7 @@
     "See messages posted to this room": "Zie berichten verstuurd naar dit gesprek",
     "Send messages as you in your active room": "Stuur berichten als uzelf in uw actieve gesprek",
     "Send messages as you in this room": "Stuur berichten als uzelf in dit gesprek",
-    "End": "Beëindigen",
+    "End": "End",
     "The <b>%(capability)s</b> capability": "De <b>%(capability)s</b> mogelijkheid",
     "See <b>%(eventType)s</b> events posted to your active room": "Stuur <b>%(eventType)s</b> gebeurtenissen verstuurd in uw actieve gesprek",
     "Send <b>%(eventType)s</b> events as you in your active room": "Stuur <b>%(eventType)s</b> gebeurtenissen als uzelf in uw actieve gesprek",
@@ -2648,6 +2648,307 @@
     "Feedback sent": "Feedback verstuurd",
     "Workspace: <networkLink/>": "Werkplaats: <networkLink/>",
     "Your firewall or anti-virus is blocking the request.": "Uw firewall of antivirussoftware blokkeert de aanvraag.",
-    "Show chat effects": "Toon gesprekseffecten",
-    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Stel de naam in van een lettertype dat op uw systeem is geïnstalleerd en %(brand)s zal proberen het te gebruiken."
+    "Show chat effects": "Gesprekseffecten tonen",
+    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Stel de naam in van een lettertype dat op uw systeem is geïnstalleerd en %(brand)s zal proberen het te gebruiken.",
+    "Toggle this dialog": "Dit dialoogvenster in- of uitschakelen",
+    "Toggle right panel": "Rechterpaneel in- of uitschakelen",
+    "Toggle the top left menu": "Het menu linksboven in- of uitschakelen",
+    "Toggle video on/off": "Video in- of uitschakelen",
+    "Toggle microphone mute": "Microfoon dempen in- of uitschakelen",
+    "Toggle Quote": "Quote in- of uitschakelen",
+    "Toggle Italics": "Cursief in- of uitschakelen",
+    "Toggle Bold": "Vetgedrukt in- of uitschakelen",
+    "Go to Home View": "Ga naar welkomscherm",
+    "Activate selected button": "Geselecteerde knop activeren",
+    "Close dialog or context menu": "Dialoogvenster of contextmenu sluiten",
+    "Previous/next room or DM": "Vorige/volgende gesprek",
+    "Previous/next unread room or DM": "Vorige/volgende ongelezen gesprek",
+    "Clear room list filter field": "Gespreklijst filter wissen",
+    "Expand room list section": "Gespreklijst selectie uitvouwen",
+    "Collapse room list section": "Gespreklijst selectie invouwen",
+    "Select room from the room list": "Gesprek uit de gespreklijst selecteren",
+    "Navigate up/down in the room list": "Omhoog/omlaag in de gespreklijst navigeren",
+    "Jump to room search": "Ga naar gesprek zoeken",
+    "Search (must be enabled)": "Zoeken (moet zijn ingeschakeld)",
+    "Upload a file": "Bestand uploaden",
+    "Jump to oldest unread message": "Ga naar het oudste ongelezen bericht",
+    "Dismiss read marker and jump to bottom": "Verlaat de leesmarkering en spring naar beneden",
+    "Scroll up/down in the timeline": "Omhoog/omlaag in de tijdlijn scrollen",
+    "Cancel replying to a message": "Antwoord op bericht annuleren",
+    "Navigate composer history": "Berichtveldgeschiedenis navigeren",
+    "Jump to start/end of the composer": "Ga naar begin/eind van de berichtveld",
+    "Navigate recent messages to edit": "Navigeer recente berichten om te bewerken",
+    "New line": "Nieuwe regel",
+    "Page Up": "Page Up",
+    "Page Down": "Page Down",
+    "Esc": "Esc",
+    "Enter": "Enter",
+    "Space": "Spatie",
+    "Ctrl": "Ctrl",
+    "Super": "Super",
+    "Shift": "Shift",
+    "Alt Gr": "Alt Gr",
+    "Alt": "Alt",
+    "Cancel autocomplete": "Autoaanvullen annuleren",
+    "Move autocomplete selection up/down": "Autoaanvullen selectie omhoog/omlaag verplaatsen",
+    "Autocomplete": "Autoaanvullen",
+    "Room List": "Gespreklijst",
+    "Calls": "Oproepen",
+    "Navigation": "Navigatie",
+    "Currently indexing: %(currentRoom)s": "Momenteel indexeren: %(currentRoom)s",
+    "Enter your recovery passphrase a second time to confirm it.": "Voer uw herstel wachtwoord een tweede keer in om te bevestigen.",
+    "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Deze sessie heeft ontdekt dat uw veiligheidszin en sleutel voor versleutelde berichten zijn verwijderd.",
+    "A new Security Phrase and key for Secure Messages have been detected.": "Er is een nieuwe veiligheidszin en sleutel voor versleutelde berichten gedetecteerd.",
+    "Save your Security Key": "Uw veiligheidssleutel opslaan",
+    "Confirm Security Phrase": "Veiligheidszin bevestigen",
+    "Set a Security Phrase": "Een veiligheidszin instellen",
+    "You can also set up Secure Backup & manage your keys in Settings.": "U kunt ook een beveiligde back-up instellen en uw sleutels beheren via instellingen.",
+    "Secure Backup": "Beveiligde back-up",
+    "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Geen toegang tot sleutelopslag. Controleer of u de juiste veiligheidszin hebt ingevoerd.",
+    "Secret storage:": "Sleutelopslag:",
+    "Unable to query secret storage status": "Kan status sleutelopslag niet opvragen",
+    "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Bewaar uw veiligheidssleutel op een veilige plaats, zoals in een wachtwoordmanager of een kluis, aangezien deze wordt gebruikt om uw versleutelde gegevens te beveiligen.",
+    "Confirm your recovery passphrase": "Bevestig uw herstel wachtwoord",
+    "Use a different passphrase?": "Gebruik een ander wachtwoord?",
+    "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.": "Voer een veiligheidszin in die alleen u kent, aangezien deze wordt gebruikt om uw gegevens te versleutelen. Om veilig te zijn, moet u het wachtwoord van uw account niet opnieuw gebruiken.",
+    "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Bescherm uw server tegen toegangsverlies tot versleutelde berichten en gegevens door een back-up te maken van de versleutelingssleutels.",
+    "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Gebruik een veiligheidszin die alleen u kent, en sla optioneel een veiligheidssleutel op om te gebruiken als back-up.",
+    "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Wij maken een veiligheidssleutel voor u aan die u ergens veilig kunt opbergen, zoals in een wachtwoordmanager of een kluis.",
+    "Generate a Security Key": "Genereer een veiligheidssleutel",
+    "Make a copy of your Security Key": "Maak een kopie van uw veiligheidssleutel",
+    "Confirm your Security Phrase": "Bevestig uw veiligheidszin",
+    "Secure your backup with a Security Phrase": "Beveilig uw back-up met een veiligheidszin",
+    "Your Security Key is in your <b>Downloads</b> folder.": "Uw veiligheidssleutel staat in uw <b>Downloads</b>-map.",
+    "Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Uw veiligheidssleutel is <b>gekopieerd naar uw klembord</b>, plak het in:",
+    "Your Security Key": "Uw veiligheidssleutel",
+    "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Uw veiligheidssleutel is een vangnet - u kunt hem gebruiken om de toegang tot uw versleutelde berichten te herstellen als u uw veiligheidszin bent vergeten.",
+    "Repeat your Security Phrase...": "Herhaal uw veiligheidszin...",
+    "Please enter your Security Phrase a second time to confirm.": "Voer uw veiligheidszin een tweede keer in om te bevestigen.",
+    "Set up with a Security Key": "Instellen met een veiligheidssleutel",
+    "Great! This Security Phrase looks strong enough.": "Geweldig. Deze veiligheidszin ziet er sterk genoeg uit.",
+    "Enter a Security Phrase": "Veiligheidszin invoeren",
+    "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Wij bewaren een versleutelde kopie van uw sleutels op onze server. Beveilig uw back-up met een veiligheidszin.",
+    "or another cross-signing capable Matrix client": "of een andere Matrix-client die kruislings kan ondertekenen",
+    "%(brand)s Android": "%(brand)s Android",
+    "%(brand)s iOS": "%(brand)s iOS",
+    "%(brand)s Desktop": "%(brand)s Desktop",
+    "%(brand)s Web": "%(brand)s Web",
+    "This requires the latest %(brand)s on your other devices:": "Dit vereist de nieuwste %(brand)s op uw andere toestellen:",
+    "Use Security Key": "Gebruik veiligheidssleutel",
+    "Use Security Key or Phrase": "Gebruik veiligheidssleutel of -zin",
+    "Decide where your account is hosted": "Kies waar uw account wordt gehost",
+    "Host account on": "Host uw account op",
+    "Already have an account? <a>Sign in here</a>": "Heeft u al een account? <a>Aanmelden</a>",
+    "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s of %(usernamePassword)s",
+    "Continue with %(ssoButtons)s": "Ga verder met %(ssoButtons)s",
+    "That username already exists, please try another.": "Die gebruikersnaam bestaat al, probeer een andere.",
+    "New? <a>Create account</a>": "Nieuw? <a>Maak een account aan</a>",
+    "If you've joined lots of rooms, this might take a while": "Als u zich bij veel gesprekken heeft aangesloten, kan dit een tijdje duren",
+    "Signing In...": "Aanmelden...",
+    "Syncing...": "Synchroniseren...",
+    "There was a problem communicating with the homeserver, please try again later.": "Er was een communicatieprobleem met de homeserver, probeer het later opnieuw.",
+    "Community and user menu": "Gemeenschaps- en gebruikersmenu",
+    "User menu": "Gebruikersmenu",
+    "Switch theme": "Thema wisselen",
+    "Community settings": "Gemeenschapsinstellingen",
+    "User settings": "Gebruikersinstellingen",
+    "Security & privacy": "Veiligheid & privacy",
+    "New here? <a>Create an account</a>": "Nieuw hier? <a>Maak een account</a>",
+    "Got an account? <a>Sign in</a>": "Heeft u een account? <a>Aanmelden</a>",
+    "Failed to find the general chat for this community": "De algemene chat voor deze gemeenschap werd niet gevonden",
+    "Filter rooms and people": "Gespreken en personen filteren",
+    "Explore rooms in %(communityName)s": "Ontdek de gesprekken van %(communityName)s",
+    "delete the address.": "het adres verwijderen.",
+    "Delete the room address %(alias)s and remove %(name)s from the directory?": "Het gespreksadres %(alias)s en %(name)s uit de catalogus verwijderen?",
+    "You have no visible notifications.": "U hebt geen zichtbare meldingen.",
+    "You’re all caught up": "U bent helemaal bij",
+    "Self-verification request": "Verzoek om zelfverificatie",
+    "You do not have permission to create rooms in this community.": "U hebt geen toestemming om gesprekken te maken in deze gemeenschap.",
+    "Cannot create rooms in this community": "Kan geen gesprek maken in deze gemeenschap",
+    "Upgrade to pro": "Upgrade naar pro",
+    "Now, let's help you get started": "Laten we u helpen om te beginnen",
+    "Welcome %(name)s": "Welkom %(name)s",
+    "Add a photo so people know it's you.": "Voeg een foto toe zodat mensen weten dat u het bent.",
+    "Great, that'll help people know it's you": "Geweldig, dat zal mensen helpen te weten dat u het bent",
+    "<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 voor de pagina van uw gemeenschap</h1>\n<p>\nGebruik de lange beschrijving om nieuwe deelnemers voor te stellen aan de gemeenschap, of verspreid enkele belangrijke <a href=\"foo\">links</a>\n</p>\n<p>\nU kunt zelfs afbeeldingen toevoegen met Matrix URL's <img src=\"mxc://url\" />\n</p>\n",
+    "Create community": "Gemeenschap aanmaken",
+    "Attach files from chat or just drag and drop them anywhere in a room.": "Voeg bestanden toe vanuit het gesprek of sleep ze in een gesprek.",
+    "No files visible in this room": "Geen bestanden zichtbaar in dit gesprek",
+    "Sign in with SSO": "Aanmelden met SSO",
+    "Use email to optionally be discoverable by existing contacts.": "Gebruik e-mail om optioneel ontdekt te worden door bestaande contacten.",
+    "Use email or phone to optionally be discoverable by existing contacts.": "Gebruik e-mail of telefoon om optioneel ontdekt te kunnen worden door bestaande contacten.",
+    "Add an email to be able to reset your password.": "Voeg een e-mail toe om uw wachtwoord te kunnen resetten.",
+    "Forgot password?": "Wachtwoord vergeten?",
+    "That phone number doesn't look quite right, please check and try again": "Dat telefoonnummer ziet er niet goed uit, controleer het en probeer het opnieuw",
+    "Enter phone number": "Telefoonnummer invoeren",
+    "Enter email address": "E-mailadres invoeren",
+    "Something went wrong in confirming your identity. Cancel and try again.": "Er is iets misgegaan bij het bevestigen van uw identiteit. Annuleer en probeer het opnieuw.",
+    "Open the link in the email to continue registration.": "Open de link in de e-mail om verder te gaan met de registratie.",
+    "A confirmation email has been sent to %(emailAddress)s": "Er is een bevestigingsmail verzonden naar %(emailAddress)s",
+    "Away": "Afwezig",
+    "Move right": "Ga naar rechts",
+    "Move left": "Ga naar links",
+    "Revoke permissions": "Machtigingen intrekken",
+    "Take a picture": "Neem een foto",
+    "Hold": "Vasthouden",
+    "Resume": "Hervatten",
+    "If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Als u uw veiligheidssleutel bent vergeten, kunt u <button>nieuwe herstelopties instellen</button>",
+    "Access your secure message history and set up secure messaging by entering your Security Key.": "Ga naar uw veilige berichtengeschiedenis en stel veilige berichten in door uw veiligheidssleutel in te voeren.",
+    "Not a valid Security Key": "Geen geldige veiligheidssleutel",
+    "This looks like a valid Security Key!": "Dit lijkt op een geldige veiligheidssleutel!",
+    "Enter Security Key": "Veiligheidssleutel invoeren",
+    "If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Als u uw veiligheidszin bent vergeten, kunt u <button1>uw veiligheidssleutel gebruiken</button1> of <button2>nieuwe herstelopties instellen</button2>",
+    "Access your secure message history and set up secure messaging by entering your Security Phrase.": "Ga naar uw veilige berichtengeschiedenis en stel veilige berichten in door uw veiligheidszin in te voeren.",
+    "Enter Security Phrase": "Voer veiligheidszin in",
+    "Successfully restored %(sessionCount)s keys": "Succesvol %(sessionCount)s sleutels hersteld",
+    "Keys restored": "Sleutels hersteld",
+    "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "Back-up kon niet worden gedecodeerd met deze veiligheidszin: controleer of u de juiste veiligheidszin hebt ingevoerd.",
+    "Incorrect Security Phrase": "Onjuiste veiligheidszin",
+    "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "Back-up kon niet worden ontcijferd met deze veiligheidssleutel: controleer of u de juiste veiligheidssleutel hebt ingevoerd.",
+    "Security Key mismatch": "Verkeerde veiligheidssleutel",
+    "%(completed)s of %(total)s keys restored": "%(completed)s van %(total)s sleutels hersteld",
+    "Fetching keys from server...": "Sleutels ophalen van server...",
+    "Restoring keys from backup": "Sleutels herstellen vanaf back-up",
+    "Unable to set up keys": "Kan geen sleutels instellen",
+    "Click the button below to confirm setting up encryption.": "Klik op de knop hieronder om het instellen van de versleuting te bevestigen.",
+    "Confirm encryption setup": "Bevestig versleuting instelling",
+    "Use your Security Key to continue.": "Gebruik uw veiligheidssleutel om verder te gaan.",
+    "Security Key": "Veiligheidssleutel",
+    "Enter your Security Phrase or <button>Use your Security Key</button> to continue.": "Voer uw veiligheidszin in of <button>gebruik uw veiligheidssleutel</button> om verder te gaan.",
+    "Security Phrase": "Veiligheidszin",
+    "Invalid Security Key": "Ongeldige veiligheidssleutel",
+    "Wrong Security Key": "Verkeerde veiligheidssleutel",
+    "Looks good!": "Ziet er goed uit!",
+    "Wrong file type": "Verkeerd bestandstype",
+    "Remember this": "Onthoud dit",
+    "The widget will verify your user ID, but won't be able to perform actions for you:": "De widget zal uw gebruikers-ID verifiëren, maar zal geen acties voor u kunnen uitvoeren:",
+    "Allow this widget to verify your identity": "Sta deze widget toe om uw identiteit te verifiëren",
+    "Decline All": "Alles weigeren",
+    "Approve": "Goedkeuren",
+    "This widget would like to:": "Deze widget zou willen:",
+    "Approve widget permissions": "Machtigingen voor widgets goedkeuren",
+    "Use your preferred Matrix homeserver if you have one, or host your own.": "Gebruik de Matrix-homeserver van uw voorkeur als u er een hebt, of host uw eigen.",
+    "We call the places where you can host your account ‘homeservers’.": "Wij noemen de plaatsen waar u uw account kunt hosten 'homeservers'.",
+    "Unable to validate homeserver": "Kan homeserver niet valideren",
+    "Recent changes that have not yet been received": "Recente wijzigingen die nog niet zijn ontvangen",
+    "The server is not configured to indicate what the problem is (CORS).": "De server is niet geconfigureerd om aan te geven wat het probleem is (CORS).",
+    "A connection error occurred while trying to contact the server.": "Er is een verbindingsfout opgetreden tijdens het contact maken met de server.",
+    "Your area is experiencing difficulties connecting to the internet.": "Uw regio ondervindt problemen bij de verbinding met het internet.",
+    "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Uw server reageert niet op sommige van uw verzoeken. Hieronder staan enkele van de meest waarschijnlijke redenen.",
+    "We recommend you change your password and Security Key in Settings immediately": "Wij raden u aan uw wachtwoord en veiligheidssleutel in de instellingen onmiddellijk te wijzigen",
+    "Data on this screen is shared with %(widgetDomain)s": "Gegevens op dit scherm worden gedeeld met %(widgetDomain)s",
+    "Modal Widget": "Modale widget",
+    "Confirm this user's session by comparing the following with their User Settings:": "Bevestig de sessie van deze gebruiker door het volgende te vergelijken met zijn gebruikersinstellingen:",
+    "Cancelled signature upload": "Geannuleerde ondertekening upload",
+    "%(brand)s encountered an error during upload of:": "%(brand)s is een fout tegengekomen tijdens het uploaden van:",
+    "a key signature": "een sleutel ondertekening",
+    "a device cross-signing signature": "een apparaat kruiselings ondertekenen ondertekening",
+    "a new cross-signing key signature": "een nieuwe kruiselings ondertekenen ondertekening",
+    "a new master key signature": "een nieuwe hoofdsleutel ondertekening",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Dit zal ze niet uitnodigen voor %(communityName)s. Als u iemand wilt uitnodigen voor %(communityName)s, klik <a>hier</a>",
+    "Failed to transfer call": "Gesprek niet doorverbonden",
+    "A call can only be transferred to a single user.": "Een oproep kan slechts naar één gebruiker worden doorverbonden.",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Meer informatie vindt u in onze <privacyPolicyLink />, <termsOfServiceLink /> en <cookiePolicyLink />.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Door tijdelijk door te gaan, krijgt het installatieproces van %(hostSignupBrand)s toegang tot uw account om geverifieerde e-mailadressen op te halen. Deze gegevens worden niet opgeslagen.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Kan geen verbinding maken met uw homeserver. Sluit dit dialoogvenster en probeer het opnieuw.",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Weet u zeker dat u het aanmaken van de host wilt afbreken? Het proces kan niet worden voortgezet.",
+    "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: Als u een bug start, stuur ons dan <debugLogsLink>debug logs</debugLogsLink> om ons te helpen het probleem op te sporen.",
+    "There was an error updating your community. The server is unable to process your request.": "Er is een fout opgetreden bij het updaten van uw gemeenschap. De server is niet in staat om uw verzoek te verwerken.",
+    "There was an error finding this widget.": "Er is een fout opgetreden bij het vinden van deze widget.",
+    "Server did not return valid authentication information.": "Server heeft geen geldige verificatiegegevens teruggestuurd.",
+    "Server did not require any authentication": "Server heeft geen authenticatie nodig",
+    "There was a problem communicating with the server. Please try again.": "Er was een communicatie probleem met de server. Probeer het opnieuw.",
+    "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "U zou dit kunnen uitschakelen als dit gesprek gebruikt zal worden om samen te werken met externe teams die hun eigen homeserver hebben. Dit kan later niet meer veranderd worden.",
+    "Verify other session": "Verifier andere sessies",
+    "About homeservers": "Over homeservers",
+    "Learn more": "Lees verder",
+    "Other homeserver": "Andere homeserver",
+    "Sign into your homeserver": "Login op uw homeserver",
+    "Specify a homeserver": "Specificeer een homeserver",
+    "Invalid URL": "Ongeldige URL",
+    "Upload completed": "Upload voltooid",
+    "Maximize dialog": "Maximaliseer dialoog",
+    "%(hostSignupBrand)s Setup": "%(hostSignupBrand)s Installatie",
+    "You should know": "U moet weten",
+    "Privacy Policy": "Privacystatement",
+    "Cookie Policy": "Cookiebeleid",
+    "Abort": "Annuleren",
+    "Confirm abort of host creation": "Bevestig het annuleren van de host creatie",
+    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "U zou dit kunnen aanzetten als dit gesprek alleen gebruikt zal worden voor samenwerking met interne teams op uw homeserver. Dit kan later niet meer veranderd worden.",
+    "You can’t disable this later. Bridges & most bots won’t work yet.": "U kunt dit later niet uitschakelen. Bruggen en de meeste bots zullen nog niet werken.",
+    "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Er is een fout opgetreden bij het aanmaken van uw gemeenschap. De naam kan bezet zijn of de server is niet in staat om uw aanvraag te verwerken.",
+    "Preparing to download logs": "Klaarmaken om logs te downloaden",
+    "Matrix rooms": "Matrix-gesprekken",
+    "%(networkName)s rooms": "%(networkName)s gesprekken",
+    "Enter the name of a new server you want to explore.": "Voer de naam in van een nieuwe server die u wilt verkennen.",
+    "Remove server": "Server verwijderen",
+    "All rooms": "Alle gesprekken",
+    "Windows": "Windows",
+    "Screens": "Schermen",
+    "Share your screen": "Uw scherm delen",
+    "Submit logs": "Logs versturen",
+    "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Berichten in dit gesprek zijn eind-tot-eind-versleuteld. Als mensen deelnemen, kan u ze verifiëren in hun profiel, tik gewoon op hun avatar.",
+    "In encrypted rooms, verify all users to ensure it’s secure.": "Controleer alle gebruikers in versleutelde gesprekken om er zeker van te zijn dat het veilig is.",
+    "Verify all users in a room to ensure it's secure.": "Controleer alle gebruikers in een gesprek om er zeker van te zijn dat hij veilig is.",
+    "%(count)s people|one": "%(count)s persoon",
+    "Add widgets, bridges & bots": "Widgets, bruggen & bots toevoegen",
+    "Edit widgets, bridges & bots": "Widgets, bruggen & bots bewerken",
+    "Set my room layout for everyone": "Stel mijn gesprekindeling in voor iedereen",
+    "New published address (e.g. #alias:server)": "Nieuw gepubliceerd adres (b.v. #alias:server)",
+    "No other published addresses yet, add one below": "Nog geen andere gepubliceerde adressen, voeg er hieronder een toe",
+    "Other published addresses:": "Andere gepubliceerde adressen:",
+    "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.": "Gepubliceerde adressen kunnen door iedereen op elke server gebruikt worden om aan je groep deel te nemen. Om een adres te publiceren moet het eerste ingesteld worden als lokaaladres.",
+    "Published Addresses": "Gepubliceerde adressen",
+    "Mentions & Keywords": "Vermeldingen & Trefwoorden",
+    "Use the + to make a new room or explore existing ones below": "Gebruik de + om een nieuw gesprek te starten of ontdek de bestaande groepen hieronder",
+    "Open dial pad": "Kiestoetsen openen",
+    "Recently visited rooms": "Onlangs geopende gesprekken",
+    "Add a photo, so people can easily spot your room.": "Voeg een foto toe, zodat mensen je gemakkelijk kunnen herkennen in het gesprek.",
+    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Alleen u beiden nemen deel aan dit gesprek, tenzij een van u beiden iemand uitnodigt om deel te nemen.",
+    "Emoji picker": "Emoji kiezer",
+    "Room ID or address of ban list": "Gesprek-ID of het adres van de banlijst",
+    "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Voeg hier gebruikers en servers toe die u wilt negeren. Gebruik asterisken om %(brand)s met alle tekens te laten overeenkomen. Bijvoorbeeld, <code>@bot:*</code> zou alle gebruikers negeren die de naam 'bot' hebben op elke server.",
+    "Please verify the room ID or address and try again.": "Controleer het gesprek-ID of het adres en probeer het opnieuw.",
+    "Message layout": "Berichtlayout",
+    "Custom theme URL": "Aangepaste thema-URL",
+    "Error downloading theme information.": "Fout bij het downloaden van de thema-informatie.",
+    "Invalid theme schema.": "Ongeldig themaschema.",
+    "Hey you. You're the best!": "Hey. U bent de beste!",
+    "Backup key cached:": "Back-up sleutel cached:",
+    "Backup key stored:": "Back-up sleutel bewaard:",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Maak een back-up van uw encryptiesleutels met uw accountgegevens voor het geval u de toegang tot uw sessies verliest. Uw sleutels worden beveiligd met een unieke veiligheidssleutel.",
+    "well formed": "goed gevormd",
+    "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s kan versleutelde berichten niet veilig lokaal opslaan in een webbrowser. Gebruik <desktopLink>%(brand)s Desktop</desktopLink> om versleutelde berichten in zoekresultaten te laten verschijnen.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Veilig lokaal opslaan van versleutelde berichten zodat ze in de zoekresultaten verschijnen, gebruik %(size)s voor het opslaan van berichten uit %(rooms)s gesprek.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Veilig lokaal opslaan van versleutelde berichten zodat ze in de zoekresultaten verschijnen, gebruik %(size)s voor het opslaan van berichten uit %(rooms)s gesprekken.",
+    "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Elke sessie die door een gebruiker wordt gebruikt, afzonderlijk verifiëren om deze als vertrouwd aan te merken, waarbij geen vertrouwen wordt gesteld in kruiselings ondertekende apparaten.",
+    "User signing private key:": "Gebruiker ondertekening privésleutel:",
+    "Master private key:": "Hoofd privésleutel:",
+    "Self signing private key:": "Zelfondertekenende privésleutel:",
+    "Cross-signing is not set up.": "Kruiselings ondertekenen is niet ingesteld.",
+    "Cross-signing is ready for use.": "Kruiselings ondertekenen is klaar voor gebruik.",
+    "Your server isn't responding to some <a>requests</a>.": "Uw server reageert niet op sommige <a>verzoeken</a>.",
+    "Dial pad": "Kiestoetsen",
+    "%(name)s on hold": "%(name)s in de wacht",
+    "%(peerName)s held the call": "%(peerName)s heeft de oproep in de wacht",
+    "You held the call <a>Resume</a>": "U heeft een oproep in de wacht <a>Hervat</a>",
+    "You held the call <a>Switch</a>": "U heeft een oproep in de wacht <a>Wissel</a>",
+    "Sends the given message with snowfall": "Verstuur het bericht met sneeuwval",
+    "Sends the given message with fireworks": "Verstuur het bericht met vuurwerk",
+    "Sends the given message with confetti": "Verstuur het bericht met confetti",
+    "IRC display name width": "Breedte IRC-weergavenaam",
+    "Enable experimental, compact IRC style layout": "Compacte IRC-layout (experimenteel) inschakelen",
+    "Minimize dialog": "Dialoog minimaliseren",
+    "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Als u nu annuleert, kunt u versleutelde berichten en gegevens verliezen als u geen toegang meer heeft tot uw login.",
+    "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Bevestig uw identiteit door deze login te verifiëren vanuit een van uw andere sessies, waardoor u toegang krijgt tot uw versleutelde berichten.",
+    "Verify this login": "Controleer deze login",
+    "To continue, use Single Sign On to prove your identity.": "Om verder te gaan, gebruik uw eenmalige aanmelding om uw identiteit te bewijzen.",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Plakt ( ͡° ͜ʖ ͡°) vóór een bericht zonder opmaak",
+    "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Plakt ┬──┬ ノ( ゜-゜ノ) vóór een bericht zonder opmaak",
+    "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Plakt (╯°□°)╯︵ ┻━┻ vóór een bericht zonder opmaak",
+    "Liberate your communication": "Bevrijd uw communicatie",
+    "Create a Group Chat": "Maak een groepsgesprek aan",
+    "Send a Direct Message": "Start een direct gesprek",
+    "Welcome to %(appName)s": "Welkom bij %(appName)s",
+    "<a>Add a topic</a> to help people know what it is about.": "<a>Stel een gespreksonderwerp in</a> zodat mensen weten waar het over gaat."
 }

From 5cd6426645167c42c473cd30e1ebd3f1cbfc97ba Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Sun, 21 Feb 2021 19:45:05 +0000
Subject: [PATCH 131/389] Translated using Weblate (Russian)

Currently translated at 99.7% (2757 of 2764 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 | 76 ++++++++++++++++++++++++++++++++++++++--
 1 file changed, 74 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 7a889ed084..a72eef2c58 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -2635,7 +2635,7 @@
     "Server Options": "Параметры сервера",
     "Decline All": "Отклонить все",
     "Homeserver": "Домашний сервер",
-    "Approve": "Одобрить",
+    "Approve": "Согласиться",
     "Approve widget permissions": "Одобрить разрешения виджета",
     "Send stickers into your active room": "Отправить стикеры в активную комнату",
     "Remain on your screen while running": "Оставаться на экране во время работы",
@@ -2985,5 +2985,77 @@
     "Unable to look up phone number": "Невозможно найти номер телефона",
     "Change which room, message, or user you're viewing": "Измените комнату, сообщение или пользователя, которого вы просматриваете",
     "Channel: <channelLink/>": "Канал: <channelLink/>",
-    "Workspace: <networkLink/>": "Рабочая область: <networkLink/>"
+    "Workspace: <networkLink/>": "Рабочая область: <networkLink/>",
+    "Search (must be enabled)": "Поиск (должен быть включен)",
+    "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Этот сеанс обнаружил, что ваша секретная фраза и ключ безопасности для защищенных сообщений были удалены.",
+    "A new Security Phrase and key for Secure Messages have been detected.": "Обнаружены новая секретная фраза и ключ безопасности для защищенных сообщений.",
+    "Make a copy of your Security Key": "Сделайте копию своего ключа безопасности",
+    "Confirm your Security Phrase": "Подтвердите секретную фразу",
+    "Secure your backup with a Security Phrase": "Защитите свою резервную копию с помощью секретной фразы",
+    "Your Security Key is in your <b>Downloads</b> folder.": "Ваш ключ безопасности находится в папке <b>Загрузки</b>.",
+    "Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Ваш ключ безопасности был <b>скопирован в буфер обмена</b>, вставьте его в:",
+    "Your Security Key": "Ваш ключ безопасности",
+    "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Ваш ключ безопасности является защитной сеткой - вы можете использовать его для восстановления доступа к своим зашифрованным сообщениям, если вы забыли секретную фразу.",
+    "Repeat your Security Phrase...": "Повторите секретную фразу…",
+    "Please enter your Security Phrase a second time to confirm.": "Пожалуйста, введите секретную фразу второй раз для подтверждения.",
+    "Set up with a Security Key": "Настройка с помощью ключа безопасности",
+    "Great! This Security Phrase looks strong enough.": "Отлично! Эта контрольная фраза выглядит достаточно сильной.",
+    "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Мы будем хранить зашифрованную копию ваших ключей на нашем сервере. Защитите свою резервную копию с помощью секретной фразы.",
+    "Use Security Key": "Используйте ключ безопасности",
+    "Use Security Key or Phrase": "Используйте ключ безопасности или секретную фразу",
+    "Upgrade to pro": "Перейти на Pro",
+    "Allow this widget to verify your identity": "Разрешите этому виджету проверить ваш идентификатор",
+    "Something went wrong in confirming your identity. Cancel and try again.": "Что-то пошло не так при вашей идентификации. Отмените последнее действие и попробуйте еще раз.",
+    "If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Если вы забыли свой ключ безопасности, вы можете <button>настроить новые параметры восстановления</button>",
+    "Access your secure message history and set up secure messaging by entering your Security Key.": "Получите доступ к своей истории защищенных сообщений и настройте безопасный обмен сообщениями, введя ключ безопасности.",
+    "Not a valid Security Key": "Неправильный ключ безопасности",
+    "This looks like a valid Security Key!": "Похоже, это правильный ключ безопасности!",
+    "Enter Security Key": "Введите ключ безопасности",
+    "If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Если вы забыли секретную фразу, вы можете <button1>использовать ключ безопасности</button1> или <button2>настроить новые параметры восстановления</button2>",
+    "Access your secure message history and set up secure messaging by entering your Security Phrase.": "Получите доступ к своей истории защищенных сообщений и настройте безопасный обмен сообщениями, введя секретную фразу.",
+    "Enter Security Phrase": "Введите секретную фразу",
+    "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "Не удалось расшифровать резервную копию с помощью этой секретной фразы: убедитесь, что вы ввели правильную секретную фразу.",
+    "Incorrect Security Phrase": "Неверная секретная фраза",
+    "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "Не удалось расшифровать резервную копию с помощью этого ключа безопасности: убедитесь, что вы ввели правильный ключ безопасности.",
+    "Security Key mismatch": "Ключ безопасности не подходит",
+    "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Невозможно получить доступ к секретному хранилищу. Убедитесь, что вы ввели правильную секретную фразу.",
+    "Invalid Security Key": "Неверный ключ безопасности",
+    "Wrong Security Key": "Неправильный ключ безопасности",
+    "Remember this": "Запомнить это",
+    "The widget will verify your user ID, but won't be able to perform actions for you:": "Виджет проверит ваш идентификатор пользователя, но не сможет выполнять за вас действия:",
+    "We recommend you change your password and Security Key in Settings immediately": "Мы рекомендуем вам немедленно сменить пароль и ключ безопасности в настройках",
+    "Minimize dialog": "Свернуть диалог",
+    "Maximize dialog": "Развернуть диалог",
+    "%(hostSignupBrand)s Setup": "%(hostSignupBrand)s Настройка",
+    "You should know": "Вы должны знать",
+    "Privacy Policy": "Политика конфиденциальности",
+    "Cookie Policy": "Политика использования файлов cookie",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Дополнительную информацию можно найти на страницах <privacyPolicyLink />,<termsOfServiceLink /> и <cookiePolicyLink />.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Продолжая процесс настройки %(hostSignupBrand)s, вы предоставите доступ к вашей учётной записи для получения проверенных адресов электронной почты. Эти данные не сохраняются.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Не удалось подключиться к домашнему серверу. Закройте это диалоговое окно и попробуйте ещё раз.",
+    "Abort": "Отмена",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Вы уверены, что хотите прервать создание хоста? Процесс не может быть продолжен.",
+    "Confirm abort of host creation": "Подтвердите отмену создания хоста",
+    "Windows": "Окна",
+    "Screens": "Экраны",
+    "Share your screen": "Поделитесь своим экраном",
+    "Set my room layout for everyone": "Установить мой макет комнаты для всех",
+    "Recently visited rooms": "Недавно посещённые комнаты",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Сделайте резервную копию ключей шифрования с данными вашей учетной записи на случай, если вы потеряете доступ к своим сеансам. Ваши ключи будут защищены уникальным ключом безопасности.",
+    "Use Ctrl + F to search": "Нажмите Ctrl + F для поиска",
+    "Use Command + F to search": "Нажмите Command (⌘) + F для поиска",
+    "Show line numbers in code blocks": "Показывать номера строк в блоках кода",
+    "Expand code blocks by default": "По умолчанию отображать блоки кода целиком",
+    "Show stickers button": "Показать кнопку стикеров",
+    "Use app": "Использовать приложение",
+    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web - экспериментальная версия на мобильных устройствах. Для лучшего опыта и новейших функций используйте наше бесплатное приложение.",
+    "Use app for a better experience": "Используйте приложение для лучшего опыта",
+    "%(senderName)s has updated the widget layout": "%(senderName)s обновил макет виджета",
+    "Converts the DM to a room": "Преобразовать ЛС в комнату",
+    "Converts the room to a DM": "Преобразовать комнату в ЛС",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Ваш домашний сервер не позволил вам войти в систему. Это могло произойти из-за того, что вход занял слишком много времени. Пожалуйста, попробуйте снова через пару минут. Если ситуация по-прежнему не меняется, обратитесь к администратору домашнего сервера за дополнительной информацией.",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Ваш домашний сервер был недоступен, и мы не смогли войти в систему. Пожалуйста, попробуйте снова через пару минут. Если ситуация по-прежнему не меняется, обратитесь к администратору домашнего сервера за дополнительной информацией.",
+    "Try again": "Попробовать ещё раз",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Мы попросили браузер запомнить, какой домашний сервер вы используете для входа в систему, но, к сожалению, ваш браузер забыл об этом. Перейдите на страницу входа и попробуйте ещё раз.",
+    "We couldn't log you in": "Нам не удалось войти в систему"
 }

From 940bfdf21f22bbf53798b054623d042a7ecae4c2 Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Wed, 17 Feb 2021 19:59:18 +0000
Subject: [PATCH 132/389] Translated using Weblate (Swedish)

Currently translated at 100.0% (2764 of 2764 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 | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index ae523205d5..916de9ecce 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2983,5 +2983,18 @@
     "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Din hemserver kunde inte nås så du kunde inte loggas in. Vänligen försök igen. Om det här fortsätter, vänligen kontakta administratören för din hemserver.",
     "Try again": "Försök igen",
     "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Vi bad webbläsaren att komma ihåg vilken hemserver du använder för att logga in, men tyvärr har din webbläsare glömt det. Gå till inloggningssidan och försök igen.",
-    "We couldn't log you in": "Vi kunde inte logga in dig"
+    "We couldn't log you in": "Vi kunde inte logga in dig",
+    "Upgrade to pro": "Uppgradera till pro",
+    "Minimize dialog": "Minimera dialog",
+    "Maximize dialog": "Maximera dialog",
+    "%(hostSignupBrand)s Setup": "inställning av %(hostSignupBrand)s",
+    "You should know": "Du behöver veta",
+    "Privacy Policy": "sekretesspolicy",
+    "Cookie Policy": "kakpolicy",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Läs mer i våran <privacyPolicyLink />, <termsOfServiceLink /> och <cookiePolicyLink />.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Om du fortsätter så tillåts inställningsprocessen för %(hostSignupBrand)s att temporärt komma åt din konto för att hämta verifierade e-postadresser. Den här datan lagras inte.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Misslyckades att ansluta till din hemserver. Vänligen stäng den här dialogrutan och försök igen.",
+    "Abort": "Avbryt",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Är du säker på att du vill avbryta skapande av värden? Processen kan inte fortsättas.",
+    "Confirm abort of host creation": "Bekräfta avbrytning av värdskapande"
 }

From 4a038ec2f4a1b333c43725e5c194245881a39338 Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Fri, 19 Feb 2021 01:48:12 +0000
Subject: [PATCH 133/389] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2764 of 2764 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 | 17 +++++++++++++++--
 1 file changed, 15 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index c1fc44dc05..b11ff2274f 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -2828,7 +2828,7 @@
     "United States": "美國",
     "United Kingdom": "英國",
     "%(creator)s created this DM.": "%(creator)s 建立了此直接訊息。",
-    "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their 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.": "此聊天室中的訊息為端到端加密。當人們加入,您可以在他們的個人檔案中驗證他們,只要點擊他們的大頭照就可以了。",
     "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "此處的訊息為端到端加密。請在他們的個人檔案中驗證 %(displayName)s,只要點擊他們的大頭照就可以了。",
     "This is the start of <roomName/>.": "這是 <roomName/> 的開頭。",
     "Add a photo, so people can easily spot your room.": "新增圖片,這樣人們就可以輕鬆發現您的聊天室。",
@@ -3055,5 +3055,18 @@
     "Share your screen": "分享您的畫面",
     "Recently visited rooms": "最近造訪過的聊天室",
     "Show line numbers in code blocks": "在程式碼區塊中顯示行號",
-    "Expand code blocks by default": "預設展開程式碼區塊"
+    "Expand code blocks by default": "預設展開程式碼區塊",
+    "Upgrade to pro": "升級到專業版",
+    "Minimize dialog": "最小化對話框",
+    "Maximize dialog": "最大化對話框",
+    "%(hostSignupBrand)s Setup": "%(hostSignupBrand)s 設定",
+    "You should know": "您應該知道",
+    "Privacy Policy": "隱私權政策",
+    "Cookie Policy": "Cookie 政策",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "在我們的 <privacyPolicyLink />、<termsOfServiceLink /> 與 <cookiePolicyLink /> 取得更多資訊。",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "暫時繼續進行讓 %(hostSignupBrand)s 設定流程可以存取您的帳號來擷取已驗證的電子郵件地址。此資料未儲存。",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "無法連線到您的家伺服器。請關閉對話框並再試一次。",
+    "Abort": "中止",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "您確定您想要中止主機建立嗎?流程將無法繼續。",
+    "Confirm abort of host creation": "確認中止主機建立"
 }

From 3b8ee7fa387d283f9f0d00cc178459abb6cfe355 Mon Sep 17 00:00:00 2001
From: eopo807 <arsa019@vivaldi.net>
Date: Wed, 10 Feb 2021 19:44:20 +0000
Subject: [PATCH 134/389] Translated using Weblate (Ukrainian)

Currently translated at 53.9% (1490 of 2764 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 | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index f07bd8b05b..adb830c123 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -1596,5 +1596,6 @@
     "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Повторно запитати ключі шифрування</requestLink> з інших сеансів.",
     "Enable encryption?": "Увімкнути шифрування?",
     "Enable room encryption": "Увімкнути шифрування кімнати",
-    "Encryption": "Шифрування"
+    "Encryption": "Шифрування",
+    "Try again": "Спробувати ще раз"
 }

From 1f538607f474d0f38a0b19549f5ee714f6aee546 Mon Sep 17 00:00:00 2001
From: vejetaryenvampir <vejetaryenvampir@airmail.cc>
Date: Wed, 17 Feb 2021 22:42:40 +0000
Subject: [PATCH 135/389] Translated using Weblate (Turkish)

Currently translated at 83.4% (2307 of 2764 strings)

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

diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json
index 5cedd8f322..c5316ee2df 100644
--- a/src/i18n/strings/tr.json
+++ b/src/i18n/strings/tr.json
@@ -2279,7 +2279,7 @@
     "See <b>%(eventType)s</b> events posted to this room": "Bu odaya gönderilen <b>%(eventType)s</b> türü etkinlikleri gör",
     "with state key %(stateKey)s": "%(stateKey)s durum anahtarı ile",
     "Verify all users in a room to ensure it's secure.": "Güvenli olduğuna emin olmak için odadaki tüm kullanıcıları onaylayın.",
-    "No recent messages by %(user)s found": "",
+    "No recent messages by %(user)s found": "%(user)s kullanıcısın hiç yeni ileti yok",
     "Show %(count)s more|one": "%(count)s adet daha fazla göster",
     "Show %(count)s more|other": "%(count)s adet daha fazla göster",
     "Activity": "Aktivite",
@@ -2424,5 +2424,98 @@
     "Add a new server...": "Yeni sunucu ekle...",
     "Preparing to download logs": "Loglar indirilmeye hazırlanıyor",
     "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "Hatırlatma:Tarayıcınız desteklenmiyor, deneyiminiz öngörülemiyor.",
-    "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Hata ayıklama günlükleri, kullanıcı adınız, ziyaret ettiğiniz oda veya grupların kimlikleri veya takma adları ve diğer kullanıcıların kullanıcı adları dahil olmak üzere uygulama kullanım verilerini içerir. Mesaj içermezler."
+    "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Hata ayıklama günlükleri, kullanıcı adınız, ziyaret ettiğiniz oda veya grupların kimlikleri veya takma adları ve diğer kullanıcıların kullanıcı adları dahil olmak üzere uygulama kullanım verilerini içerir. Mesaj içermezler.",
+    "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Bir oda için şifreleme bir kez etkinleştirildiğinde geri alınamaz. Şifrelenmiş bir odada gönderilen iletiler yalnızca ve yalnızca odadaki kullanıcılar tarafından görülebilir. Şifrelemeyi etkinleştirmek bir çok bot'un ve köprülemenin doğru çalışmasını etkileyebilir. <a>Şifrelemeyle ilgili daha fazla bilgi edinmek için.</a>",
+    "Link this email with your account in Settings to receive invites directly in %(brand)s.": "Doğrudan %(brand)s uygulamasından davet isteği almak için bu e-posta adresini Ayarlardan kendi hesabınıza bağlayın.",
+    "Failed to remove '%(roomName)s' from %(groupId)s": "",
+    "This client does not support end-to-end encryption.": "Bu istemci uçtan uca şifrelemeyi desteklemiyor.",
+    "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?": "Bu kullanıcı etkisizleştirmek onu bir daha oturum açmasını engeller.Ek olarak da bulundukları bütün odalardan atılırlar. Bu eylem geri dönüştürülebilir. Bu kullanıcıyı etkisizleştirmek istediğinize emin misiniz?",
+    "Disinvite this user from community?": "Bu kullanıcının bu topluluğa davetini iptal et?",
+    "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Çok sayıda ileti için bu biraz sürebilir. Lütfen bu sürede kullandığınız istemciyi yenilemeyin.",
+    "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "%(user)s kullanıcısından %(count)s sayıda ileti silmek üzeresiniz. Bu işlem geri döndürülemez. Sürdürmek istiyor musunuz?",
+    "Remove recent messages by %(user)s": "%(user)s kullanıcısından en son iletileri kaldır",
+    "Try scrolling up in the timeline to see if there are any earlier ones.": "Daha önceden kalma iletilerin var olup olmadığını kontrol etmek için zaman çizelgesinde yukarı doğru kaydırın.",
+    "Set my room layout for everyone": "Oda düzenimi herkes için ayarla",
+    "Yours, or the other users’ internet connection": "Sizin ya da diğer kullanıcıların internet bağlantısı",
+    "The homeserver the user you’re verifying is connected to": "Doğruladığınız kullanıcının bağlı olduğunu ana sunucu:",
+    "For extra security, verify this user by checking a one-time code on both of your devices.": "Fazladan güvenlik sağlamak için bu kullanıcıyı cihazlarınızdaki tek kezlik kod ile doğrulayın.",
+    "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "Şifrelenmiş odalarda iletileriniz şifreledir ve yalnızca sizde ve gönderdiğiniz kullanıcılarda iletileri açmak için anahtarlar vardır.",
+    "Messages in this room are not end-to-end encrypted.": "Bu odadaki iletiler uçtan uca şifreli değildir.",
+    "Messages in this room are end-to-end encrypted.": "Bu odadaki iletiler uçtan uca şifrelenmiştir.",
+    "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "İletileriniz şifreledir ve yalnızca sizde ve gönderdiğiniz kullanıcılarda iletileri açmak için anahtarlar vardır.",
+    "Waiting for %(displayName)s to accept…": "%(displayName)s kullanıcısın onaylaması için bekleniliyor…",
+    "Waiting for you to accept on your other session…": "Diğer oturumunuzda onaylamanız için bekleniliyor…",
+    "URL previews are disabled by default for participants in this room.": "URL ön izlemeleri, bu odadaki kullanıcılar için varsayılan olarak devre dışı bıraktırılmıştır.",
+    "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Adanın alternatif adresini güncellerken bir hata oluştu. Bu eylem, sunucu tarafından izin verilmemiş olabilir ya da geçici bir sorun oluşmuş olabilir.",
+    "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Odanın ana adresini güncellerken bir sorun oluştu. Bu eylem, sunucu tarafından izin verilmemiş olabilir ya da geçici bir sorun oluşmuş olabilir.",
+    "Hint: Begin your message with <code>//</code> to start it with a slash.": "İpucu: İletilerinizi eğik çizgi ile başlatmak için <code>//</code> ile başlayın.",
+    "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "<code>/help</code> yazarak var olan komutları listeleyebilirsiniz. Yoksa bunu bir ileti olarak mı göndermek istemiştiniz?",
+    "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "Bu oda, <roomVersion /> oda sürümünü kullanmaktadır ve ana sunucunuz tarafından <i>tutarsız</i> olarak işaretlenmiştir.",
+    "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Bu odayı güncellerseniz bu oda kapanacak ve yerine aynı adlı, güncellenmiş bir oda geçecek.",
+    "Favourited": "Beğenilenler",
+    "Mentions & Keywords": "Değinilmeler & Anahtar Sözcükler",
+    "Share this email in Settings to receive invites directly in %(brand)s.": "Doğrdan %(brand)s uygulamasından davet isteği almak için Ayarlardan bu e-posta adresini paylaşın.",
+    "Use an identity server in Settings to receive invites directly in %(brand)s.": "Doğrudan %(brand)s uygulamasından davet isteği almak için Ayarlardan bir kimlik sunucusu belirleyin.",
+    "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Bu davet, %(roomName)s odasına %(email)s e-posta adresi üzerinden yollanmıştır ve sizinle ilgili değildir",
+    "Recently visited rooms": "En son ziyaret edilmiş odalar",
+    "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Eğer diğer oturumlarınızın bu iletiyi açacak anahtarı yoksa deşifreleyemezsiniz.",
+    "Discovery options will appear once you have added a phone number above.": "Bulunulabilirlik seçenekleri, yukarıya bir telefon numarası ekleyince ortaya çıkacaktır.",
+    "Discovery options will appear once you have added an email above.": "Bulunulabilirlik seçenekleri, yukarıya bir e-posta adresi ekleyince ortaya çıkacaktır.",
+    "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Geçmişi kimin okuyabileceğini değiştirmek yalnızca odadaki yeni iletileri etkiler. Var olan geçmiş değişmeden kalacaktır.",
+    "To link to this room, please add an address.": "Bu odaya bağlamak için lütfen bir adres ekleyin.",
+    "Remove messages sent by others": "Diğerleri tarafından gönderilen iletileri kaldır",
+    "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Odanın güç düzeyi gereksinimlerini değiştirirken bir hata ile karşılaşıldı. Yeterince yetkiniz olduğunuzdan emin olup yeniden deyin.",
+    "This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "Bu oda, iletileri hiçbir platforma köprülemiyor. <a>Daha fazla bilgi için.</a>",
+    "This room is bridging messages to the following platforms. <a>Learn more.</a>": "Bu oda, iletileri sözü edilen platformlara köprülüyor. <a>Daha fazla bilgi için.</a>",
+    "A session's public name is visible to people you communicate with": "Bir oturumun halka açık adı iletişim kurduğunuz kişilerce görülebilir",
+    "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Aşağıdan oturumlarınızın adlarını düzenleyin veya oturumlarızdan çıkın ya da <a>oturumlarınızı Kullanıcı Profilinden doğrulayın</a>.",
+    "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Sunucu yönetinciniz varsayılan olarak odalarda ve doğrudandan iletilerde uçtan uca şifrelemeyi kapadı.",
+    "Read Marker lifetime (ms)": "Okundu iminin gösterim süresi (ms)",
+    "Read Marker off-screen lifetime (ms)": "Okundu iminin ekran dışındaki gösterim süresi (ms)",
+    "Composer": "Yazan",
+    "Room ID or address of ban list": "Engelleme listesinin oda kimliği ya da adresi",
+    "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Kişisel engelleme listeniz, ileti almak istemediğiniz kullanıcı veya sunucuları bulundurur. İlk engellemenizde oda listenizde \"My Ban List\" adlı bir oda oluşturulacaktır. Engelleme listesinin yürürlüğünü sürdürmesini istiyorsanız o odada kalın.",
+    "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Kullanıcıları engelleme, hangi kullanıcıları engelleyeceğini belirleyen kurallar bulunduran bir engelleme listesi kullanılarak gerçekleşir. Bir engelleme listesine abone olmak, o listeden engellenen kullanıcıların veya sunucuların sizden gizlenmesi demektir.",
+    "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Görmezden gelmek istediğiniz kullanıcıları ya da sunucuları buraya ekleyin. %(brand)s uygulamasının herhangi bir karakteri eşleştirmesini istiyorsanız yıldız imi kullanın. Örneğin <code>@fobar:*</code>, \"foobar\" adlı kullanıcıların hepsini bütün sunucularda görmezden gelir.",
+    "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Eğer GitHub aracılığıyla bir hata bildirdiyseniz hata kayıtları sorunu bulmamızda bize yardımcı olabilir. Hata kayıtları kullanıcı adınızı, daha önceden bulunmuş olduğunuz odaların veya grupların kimliklerini veya takma adlarını ve diğer kullanıcıların adlarını içerir. Hata kayıtları ileti içermez.",
+    "Please verify the room ID or address and try again.": "Lütfen oda kimliğini ya da adresini doğrulayıp yeniden deneyin.",
+    "For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "%(brand)s uygulamasına yardımcı olmak için <a>buraya</a> tıklayın ya da aşağıdaki tuşları kullanarak bot'umuzla sohbet edin.",
+    "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Başkaları tarafından e-posta adresi ya da telefon numarası ile bulunabilmek için %(serverName)s kimlik sunucusunun Kullanım Koşullarını kabul edin.",
+    "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Parolanız başarıyla değişmiştir. Diğer oturumlarınızda yeniden oturum açmadığınız sürece anlık bildirim almayacaksınız",
+    "Flair": "Özel Yetenek",
+    "Appearance Settings only affect this %(brand)s session.": "Dış Görünüş Ayarları yalnızca bu %(brand)s oturumunu etkileyecek.",
+    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Sisteminizde bulunan bir font adı belirtiniz. %(brand)s sizin için onu kullanmaya çalışacak.",
+    "Invalid theme schema.": "Geçersiz tema taslağı.",
+    "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Bir kimlik sunucusu kullanmak isteğe bağlıdır. Eğer bir tane kullanmak istemezseniz başkaları tarafından bulunamayabilir ve başkalarını e-posta adresi ya da telefon numarası ile davet edemeyebilirsiniz.",
+    "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.": "Kimlik sunucunuz ile bağlantıyı keserseniz başkaları tarafından bulunamayabilir ve başkalarını e-posta adresi ya da telefon numarası ile davet edemeyebilirsiniz.",
+    "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Şu anda herhangi bir kimlik sunucusu kullanmıyorsunuz. Başkalarını bulmak ve başkaları tarafından bulunabilmek için aşağıya bir kimlik sunucusu ekleyin.",
+    "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Eğer <server /> kimlik sunucusunu kullanarak başkalarını bulmak ve başkalarını tarafından bulunabilmek istemiyorsanız aşağıya bir başka kimlik sunucusu giriniz.",
+    "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "Şu anda <server></server> kimlik sunucusunu kullanarak başkalarını buluyorsunuz ve başkalarını tarafından bulunabiliyorsunuz. Aşağıdan kimlik sunucunuzu değiştirebilirsiniz.",
+    "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "kimlik sunucunuza erişimi engelleyen herhangi bir eklenti (Privacy Badger gibi) için tarayıcınızı kontrol ediniz",
+    "You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "Bağlantınızı kesmeden önce <b>kişisel verilerinizi <idserver /> kimlik sunucusundan silmelisiniz</b>. Ne yazık ki <idserver /> kimlik sunucusu şu anda çevrim dışı ya da bir nedenden ötürü erişilemiyor.",
+    "Disconnect from the identity server <current /> and connect to <new /> instead?": "<current /> kimlik sunucusundan bağlantı kesilip <new /> kimlik sunucusuna bağlanılsın mı?",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Olası bir oturuma erişememe durumunu önlemek için şifreleme anahtarlarınızı hesap verilerinizle yedekleyin. Anahtarlarınız eşsiz bir güvenlik anahtarı ile güvenlenecektir.",
+    "well formed": "uygun biçimlendirilmiş",
+    "Master private key:": "Ana gizli anahtar",
+    "Cross-signing is not set up.": "Çapraz imzalama ayarlanmamış.",
+    "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Hesabınız gizli belleğinde çapraz imzalama kimliği barındırıyor ancak bu oturumda daha kullanılmış değil.",
+    "Cross-signing is ready for use.": "Çapraz imzalama zaten kullanılıyor.",
+    "Channel: <channelLink/>": "Kanal: <channelLink/>",
+    "Workspace: <networkLink/>": "Çalışma alanı: <networkLink/>",
+    "Unable to look up phone number": "Telefon numarasına bakılamadı",
+    "There was an error looking up the phone number": "Telefon numarasına bakarken bir hata oluştu",
+    "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "",
+    "Use Ctrl + F to search": "Arama yapmak için Ctrl + F",
+    "Use Command + F to search": "Arama yapmak için Command + F",
+    "Show line numbers in code blocks": "Kod bloklarında satır sayısını göster",
+    "Expand code blocks by default": "Varsayılan olarak kod bloklarını genişlet",
+    "Show stickers button": "Çıkartma tuşunu göster",
+    "Use app": "Uygulamayı kullan",
+    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web arayüzü mobil üzerinde daha tamamlanmadı. En yeni özellikler ve daha iyi bir deneyim için özgür yazılım mobil uygulamamızı kullanın.",
+    "Use app for a better experience": "Daha iyi bir deneyim için uygulamayı kullanın",
+    "%(senderName)s has updated the widget layout": "%(senderName)s widget düzenini güncelledi",
+    "Remain on your screen while running": "Uygulama çalışırken lütfen başka uygulamaya geçmeyin",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Ana sunucunuza erişilemedi ve oturum açmanıza izin verilmedi. Lütfen yeniden deneyin. Eğer hata devam ederse ana sunucunuzun yöneticisine bildirin.",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Ana sunucunuz oturum açma isteğinizi reddetti. Bunun nedeni bağlantı yavaşlığı olabilir. Lütfen yeniden deneyin. Eğer hata devam ederse ana sunucunuzun yöneticisine bildirin.",
+    "Try again": "Yeniden deneyin"
 }

From 9efebc66bf3948012f39c10400761b2ace18f2d7 Mon Sep 17 00:00:00 2001
From: Tirifto <tirifto@posteo.cz>
Date: Mon, 22 Feb 2021 18:44:54 +0000
Subject: [PATCH 136/389] Translated using Weblate (Esperanto)

Currently translated at 98.7% (2729 of 2764 strings)

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

diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json
index 748d265c89..3f99a68187 100644
--- a/src/i18n/strings/eo.json
+++ b/src/i18n/strings/eo.json
@@ -370,10 +370,10 @@
     "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)saliĝis kaj foriris",
     "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s%(count)s-foje aliĝis kaj foriris",
     "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)saliĝis kaj foriris",
-    "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s%(count)s-foje foriris kaj realiĝis",
-    "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sforiris kaj realiĝis",
-    "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s%(count)s-foje foriris kaj realiĝis",
-    "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sforiris kaj realiĝis",
+    "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s%(count)s-foje foriris kaj re-aliĝis",
+    "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s foriris kaj re-aliĝis",
+    "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s %(count)s-foje foriris kaj re-aliĝis",
+    "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s foriris kaj re-aliĝis",
     "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s%(count)s-foje rifuzis inviton",
     "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)srifuzis inviton",
     "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s%(count)s-foje rifuzis inviton",
@@ -1081,10 +1081,10 @@
     "Account management": "Administrado de kontoj",
     "This event could not be displayed": "Ĉi tiu okazo ne povis montriĝi",
     "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Bonvolu instali foliumilon <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, aŭ <safariLink>Safari</safariLink>, por la plej bona sperto.",
-    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Vi estas administranto de tiu ĉi komunumo. Sen invito de alia administranto vi ne povos realiĝi.",
+    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Vi estas administranto de tiu ĉi komunumo. Sen invito de alia administranto vi ne povos re-aliĝi.",
     "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Ŝanĝoj al viaj komunumaj <bold1>nomo</bold1> kaj <bold2>profilbildo</bold2> eble ne montriĝos al aliaj uzantoj ĝis 30 minutoj.",
     "Who can join this community?": "Kiu povas aliĝi al tiu ĉi komunumo?",
-    "This room is not public. You will not be able to rejoin without an invite.": "Ĉi tiu ĉambro ne estas publika. Vi ne povos realiĝi sen invito.",
+    "This room is not public. You will not be able to rejoin without an invite.": "Ĉi tiu ĉambro ne estas publika. Vi ne povos re-aliĝi sen invito.",
     "Can't leave Server Notices room": "Ne eblas eliri el ĉambro « Server Notices »",
     "Revoke invite": "Nuligi inviton",
     "Invited by %(sender)s": "Invitita de %(sender)s",
@@ -2859,5 +2859,152 @@
     "Call failed because webcam or microphone could not be accessed. Check that:": "Voko malsukcesis, ĉar retfilmilo aŭ mikrofono ne povis uziĝi. Kontrolu, ke:",
     "Unable to access webcam / microphone": "Ne povas aliri retfilmilon / mikrofonon",
     "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Voko malsukcesis, ĉar mikrofono ne estis uzebla. Kontrolu, ĉu mikrofono estas ĝuste konektita kaj agordita.",
-    "Unable to access microphone": "Ne povas aliri mikrofonon"
+    "Unable to access microphone": "Ne povas aliri mikrofonon",
+    "Invite by email": "Inviti per retpoŝto",
+    "Minimize dialog": "Minimumigi interagujon",
+    "Maximize dialog": "Maksimumigi interagujon",
+    "Privacy Policy": "Politiko pri privateco",
+    "Cookie Policy": "Politiko pri kuketoj",
+    "There was an error finding this widget.": "Eraris serĉado de tiu ĉi fenestraĵo.",
+    "Active Widgets": "Aktivaj fenestraĵoj",
+    "Reason (optional)": "Kialo (malnepra)",
+    "Continue with %(provider)s": "Daŭrigi per %(provider)s",
+    "Homeserver": "Hejmservilo",
+    "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.": "Vi povas uzi la proprajn elekteblojn de servilo por saluti aliajn servilojn de Matrix, specifigante la URL-on de alia hejmservilo. Tio ebligas uzi Elementon kun jama konto de Matrix ĉe alia hejmservilo.",
+    "Server Options": "Elektebloj de servilo",
+    "Windows": "Fenestroj",
+    "Screens": "Ekranoj",
+    "Share your screen": "Vidigu vian ekranon",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Sekure kaŝmemori ĉifritajn mesaĝojn loke por ke ili aperu inter serĉrezultoj, uzante %(size)s por deponi mesaĝojn el %(rooms)s ĉambroj.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Sekure kaŝmemori ĉifritajn mesaĝojn loke por ke ili aperu inter serĉrezultoj, uzante %(size)s por deponi mesaĝojn el %(rooms)s ĉambroj.",
+    "Channel: <channelLink/>": "Kanalo: <channelLink/>",
+    "Remain on your screen while running": "Resti sur via ekrano rulante",
+    "Remain on your screen when viewing another room, when running": "Resti sur via ekrano rulante, dum rigardo al alia ĉambro",
+    "Go to Home View": "Iri al ĉefpaĝo",
+    "Search (must be enabled)": "Serĉi (devas esti ŝaltita)",
+    "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Tiu ĉi salutaĵo trovis, ke viaj Sekureca frazo kaj ŝlosilo por Sekuraj mesaĝoj foriĝis.",
+    "A new Security Phrase and key for Secure Messages have been detected.": "Novaj Sekureca frazo kaj ŝlosilo por Sekuraj mesaĝoj troviĝis.",
+    "Make a copy of your Security Key": "Faru kopion de via Sekureca ŝlosilo",
+    "Confirm your Security Phrase": "Konfirmu vian Sekurecan frazon",
+    "Secure your backup with a Security Phrase": "Sekurigu vian savkopion per Sekureca frazo",
+    "Your Security Key is in your <b>Downloads</b> folder.": "Via Sekureca ŝlosilo estas en via dosierujo kun <b>Elŝutoj</b>.",
+    "Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Via Sekureca ŝlosilo <b>kopiiĝis al via tondujo</b>, algluu ĝin al:",
+    "Your Security Key": "Via Sekureca ŝlosilo",
+    "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Via Sekureca ŝlosilo estas speco de asekuro – vi povas uzi ĝin por rehavi aliron al viaj ĉifritaj mesaĝoj, se vi forgesas vian Sekurecan frazon.",
+    "Repeat your Security Phrase...": "Ripetu vian Sekurecan frazon…",
+    "Please enter your Security Phrase a second time to confirm.": "Bonvolu enigi vian Sekurecan frazon je dua fojo por konfirmi.",
+    "Set up with a Security Key": "Agordi per Sekureca ŝlosilo",
+    "Great! This Security Phrase looks strong enough.": "Bonege! La Sekureca frazo ŝajnas sufiĉe forta.",
+    "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Ni konservos ĉifritan kopion de viaj ŝlosiloj en nia servilo. Sekurigu vian savkopion per Sekureca frazo.",
+    "Use Security Key": "Uzi Sekurecan ŝlosilon",
+    "Use Security Key or Phrase": "Uzu Sekurecan ŝlosilon aŭ frazon",
+    "Decide where your account is hosted": "Decidu, kie via konto gastiĝos",
+    "Host account on": "Gastigi konton ĉe",
+    "Already have an account? <a>Sign in here</a>": "Ĉu vi jam havas konton? <a>Salutu tie ĉi</a>",
+    "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s aŭ %(usernamePassword)s",
+    "Continue with %(ssoButtons)s": "Daŭrigi per %(ssoButtons)s",
+    "That username already exists, please try another.": "Tiu uzantonomo jam ekzistas, bonvolu provi alian.",
+    "New? <a>Create account</a>": "Ĉu vi novas? <a>Kreu konton</a>",
+    "There was a problem communicating with the homeserver, please try again later.": "Eraris komunikado kun la hejmservilo, bonvolu reprovi poste.",
+    "New here? <a>Create an account</a>": "Ĉu vi novas? <a>Kreu konton</a>",
+    "Got an account? <a>Sign in</a>": "Ĉu vi havas konton? <a>Salutu</a>",
+    "Filter rooms and people": "Filtri ĉambrojn kaj personojn",
+    "You have no visible notifications.": "Vi havas neniujn videblajn sciigojn.",
+    "Upgrade to pro": "Gradaltigu al profesionala versio",
+    "Now, let's help you get started": "Nun, ni helpos al vi komenci",
+    "Welcome %(name)s": "Bonvenu, %(name)s",
+    "Add a photo so people know it's you.": "Aldonu foton, por ke oni vin rekonu.",
+    "Great, that'll help people know it's you": "Bonege, tio helpos al aliuloj scii, ke temas pri vi",
+    "Use email to optionally be discoverable by existing contacts.": "Uzu retpoŝtadreson por laŭplaĉe esti trovebla de jamaj kontaktoj.",
+    "Use email or phone to optionally be discoverable by existing contacts.": "Uzu retpoŝtadreson aŭ telefonnumeron por laŭplaĉe esti trovebla de jamaj kontaktoj.",
+    "Add an email to be able to reset your password.": "Aldonu retpoŝtadreson por ebligi rehavon de via pasvorto.",
+    "Forgot password?": "Ĉu forgesis pasvorton?",
+    "That phone number doesn't look quite right, please check and try again": "Tiu telefonnumero ne ŝajnas ĝusta, bonvolu kontroli kaj reprovi",
+    "Enter phone number": "Enigu telefonnumeron",
+    "Enter email address": "Enigu retpoŝtadreson",
+    "Something went wrong in confirming your identity. Cancel and try again.": "Io misokazis dum konfirmado de via identeco. Nuligu kaj reprovu.",
+    "Open the link in the email to continue registration.": "Por daŭrigi la registriĝon, malfermu la ligilon senditan en la retletero.",
+    "A confirmation email has been sent to %(emailAddress)s": "Konfirma retletero sendiĝis al %(emailAddress)s",
+    "Hold": "Paŭzigi",
+    "Resume": "Daŭrigi",
+    "If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Se vi forgesis vian Sekurecan ŝlosilon, vi povas <button>agordi novajn elekteblojn de rehavo</button>",
+    "Access your secure message history and set up secure messaging by entering your Security Key.": "Aliru vian historion de sekuraj mesaĝoj kaj agordu sekurajn mesaĝojn per enigo de via Sekureca ŝlosio.",
+    "Not a valid Security Key": "Tio ne estas valida Sekureca ŝlosilo",
+    "This looks like a valid Security Key!": "Tio ĉi ŝajnas esti valida Sekureca ŝlosilo!",
+    "Enter Security Key": "Enigu Sekurecan ŝlosilon",
+    "If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Se vi forgesis vian Sekurecan frazon, vi povas <button1>uzi vian Sekurecan ŝlosilon</button1> aŭ <button2>agordi novajn elekteblojn de rehavo</button2>",
+    "Access your secure message history and set up secure messaging by entering your Security Phrase.": "Aliru vian historion de sekuraj mesaĝoj kaj agordu sekurigitajn mesaĝojn per enigo de via Sekureca frazo.",
+    "Enter Security Phrase": "Enigu Sekurecan frazon",
+    "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "Ne povis malĉifri savkopion per ĉi tiu Sekureca frazo: bonvolu kontroli, ĉu vi enigis la ĝustan Sekurecan frazon.",
+    "Incorrect Security Phrase": "Malĝusta Sekureca frazo",
+    "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "Ne povis malĉifri savkopion per ĉi tiu Sekureca ŝlosilo: bonvolu kontroli, ĉu vi enigis la ĝustan Sekurecan ŝlosilon.",
+    "Security Key mismatch": "Malakordo de Sekureca ŝlosilo",
+    "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Ne povas akiri sekretandeponejon. Bonvolu kontroli, ĉu vi enigis la ĝustan Sekurecan frazon.",
+    "Invalid Security Key": "Nevalida Sekureca ŝlosilo",
+    "Wrong Security Key": "Malĝusta Sekureca ŝlosilo",
+    "Remember this": "Memoru ĉi tion",
+    "The widget will verify your user ID, but won't be able to perform actions for you:": "Ĉi tiu fenestraĵo kontrolos vian identigilon de uzanto, sed ne povos fari agojn por vi:",
+    "Allow this widget to verify your identity": "Permesu al ĉi tiu fenestraĵo kontroli vian identecon",
+    "Decline All": "Rifuzi ĉion",
+    "Approve": "Aprobi",
+    "This widget would like to:": "Ĉi tiu fenestraĵo volas:",
+    "Approve widget permissions": "Aprobi rajtojn de fenestraĵo",
+    "About homeservers": "Pri hejmserviloj",
+    "Learn more": "Ekscii plion",
+    "Use your preferred Matrix homeserver if you have one, or host your own.": "Uzu vian preferatan hejmservilon de Matrix se vi havas iun, aŭ gastigu vian propran.",
+    "Other homeserver": "Alia hejmservilo",
+    "We call the places where you can host your account ‘homeservers’.": "La lokojn, kie via konto povas gastiĝi, ni nomas «hejmserviloj».",
+    "Sign into your homeserver": "Salutu vian hejmservilon",
+    "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org estas la plej granda publika hejmservilo en la mondo, kaj estas do bona loko por multaj.",
+    "Specify a homeserver": "Specifu hejmservilon",
+    "%(hostSignupBrand)s Setup": "Agordoj de %(hostSignupBrand)s",
+    "You should know": "Vi sciu",
+    "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Mesaĝoj en ĉi tiu ĉambro estas tutvoje ĉifrataj. Kiam oni aliĝas, vi povas kontroli ĝin per ĝia profilo; simple tuŝetu ĝian profilbildon.",
+    "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Mesaĝoj ĉi tie estas tutvoje ĉifritaj. Kontrolu uzanton %(displayName)s per ĝia profilo – tuŝetu ĝian profilbildon.",
+    "Role": "Rolo",
+    "Use the + to make a new room or explore existing ones below": "Uzu la simbolon + por krei novan ĉambron aŭ esplori jam ekzistantajn sube",
+    "Start a new chat": "Komenci novan babilon",
+    "Start a Conversation": "Komenci interparolon",
+    "Recently visited rooms": "Freŝe vizititiaj ĉambroj",
+    "This is the start of <roomName/>.": "Jen la komenco de <roomName/>.",
+    "Add a photo, so people can easily spot your room.": "Aldonu foton, por ke oni facile trovu vian ĉambron.",
+    "%(displayName)s created this room.": "%(displayName)s kreis ĉi tiun ĉambron.",
+    "You created this room.": "Vi kreis ĉi tiun ĉambron.",
+    "<a>Add a topic</a> to help people know what it is about.": "<a>Aldonu temon</a>, por ke oni sciu, pri kio temas.",
+    "Topic: %(topic)s ": "Temo: %(topic)s ",
+    "Topic: %(topic)s (<a>edit</a>)": "Temo: %(topic)s (<a>redakti</a>)",
+    "This is the beginning of your direct message history with <displayName/>.": "Jen la komenco de historio de viaj rektaj mesaĝoj kun <displayName/>.",
+    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Nur vi du partoprenas ĉi tiun interparolon, se neniu el vi invitos aliulon.",
+    "There was an error looking up the phone number": "Eraris trovado de la telefonnumero",
+    "Unable to look up phone number": "Ne povas trovi telefonnumeron",
+    "sends snowfall": "sendas neĝadon",
+    "Sends the given message with snowfall": "Sendas la mesaĝon kun neĝado",
+    "sends fireworks": "sendas artfajraĵon",
+    "Sends the given message with fireworks": "Sendas la mesaĝon kun artfajraĵo",
+    "sends confetti": "sendas konfetojn",
+    "Sends the given message with confetti": "Sendas la mesaĝon kun konfetoj",
+    "Show chat effects": "Montri efektojn de babilujo",
+    "Show line numbers in code blocks": "Montri numerojn de linioj en kodujoj",
+    "Expand code blocks by default": "Implicite etendi kodujojn",
+    "Show stickers button": "Butono por montri glumarkojn",
+    "Use app": "Uzu aplikaĵon",
+    "Use app for a better experience": "Uzu aplikaĵon por pli bona sperto",
+    "Don't miss a reply": "Ne preterpasu respondon",
+    "See emotes posted to your active room": "Vidi mienojn afiŝitajn al via aktiva ĉambro",
+    "See emotes posted to this room": "Vidi mienojn afiŝitajn al ĉi tiu ĉambro",
+    "Send emotes as you in your active room": "Sendi mienon kiel vi en via aktiva ĉambro",
+    "Send emotes as you in this room": "Sendi mienon kiel vi en ĉi tiu ĉambro",
+    "See text messages posted to your active room": "Vidi tekstajn mesaĝojn afiŝitajn al via aktiva ĉambro",
+    "See text messages posted to this room": "Vidi tekstajn mesaĝojn afiŝitajn al ĉi tiu ĉambro",
+    "Send text messages as you in your active room": "Sendi tekstajn mesaĝojn kiel vi en via aktiva ĉambro",
+    "Send text messages as you in this room": "Sendi tekstajn mesaĝojn kiel vi en ĉi tiu ĉambro",
+    "Change which room, message, or user you're viewing": "Ŝanĝu, kiun ĉambron, mesaĝon, aŭ uzanton vi rigardas",
+    "%(senderName)s has updated the widget layout": "%(senderName)s ĝisdatigis la aranĝon de la fenestrajoj",
+    "Converts the DM to a room": "Igas la ĉambron nerekta",
+    "Converts the room to a DM": "Igas la ĉambron rekta",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Via hejmservilo rifuzis vian saluton. Eble tio okazis, ĉar ĝi simple daŭris tro longe. Bonvolu reprovi. Se tio daŭros, bonvolu kontakti la administranton de via hejmservilo.",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Via hejmservilo estis neatingebla kaj ne povis vin salutigi. Bonvolu reprovi. Se tio daŭros, bonvolu kontakti la administranton de via hejmservilo.",
+    "Try again": "Reprovu",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Ni petis la foliumilon memori, kiun hejmservilon vi uzas por saluti, sed domaĝe, via foliumilo forgesis. Iru al la saluta paĝo kaj reprovu.",
+    "We couldn't log you in": "Ni ne povis salutigi vin"
 }

From 0ae194981a18aa6806d78b6eaa4ed155e726bd6c Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Mon, 15 Feb 2021 10:54:08 +0000
Subject: [PATCH 137/389] Translated using Weblate (Italian)

Currently translated at 100.0% (2764 of 2764 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 | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index a280709430..2764863e77 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -3045,5 +3045,25 @@
     "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Il tuo homeserver è irraggiungibile e non ha potuto farti accedere. Riprova. Se il problema persiste, contatta l'amministratore dell'homeserver.",
     "Try again": "Riprova",
     "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Abbiamo chiesto al browser di ricordare quale homeserver usi per farti accedere, ma sfortunatamente l'ha dimenticato. Vai alla pagina di accesso e riprova.",
-    "We couldn't log you in": "Non abbiamo potuto farti accedere"
+    "We couldn't log you in": "Non abbiamo potuto farti accedere",
+    "Upgrade to pro": "Aggiorna a Pro",
+    "Minimize dialog": "Riduci finestra",
+    "Maximize dialog": "Espandi finestra",
+    "%(hostSignupBrand)s Setup": "Configurazione di %(hostSignupBrand)s",
+    "You should know": "Dovresti sapere",
+    "Privacy Policy": "Informativa sulla privacy",
+    "Cookie Policy": "Informativa sui cookie",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Maggiori informazioni nella nostra <privacyPolicyLink />, <termsOfServiceLink /> e <cookiePolicyLink />.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Continuando permetti temporaneamente al processo di configurazione di %(hostSignupBrand)s di accedere al tuo account per rilevare gli indirizzi email verificati. Questi dati non vengono memorizzati.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Connessione al tuo homeserver fallita. Chiudi questo messaggio e riprova.",
+    "Abort": "Annulla",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Vuoi veramente annullare la creazione dell'host? Il processo non può essere continuato.",
+    "Confirm abort of host creation": "Conferma annullamento creazione host",
+    "Windows": "Finestre",
+    "Screens": "Schermi",
+    "Share your screen": "Condividi lo schermo",
+    "Recently visited rooms": "Stanze visitate di recente",
+    "Show line numbers in code blocks": "Mostra numeri di riga nei blocchi di codice",
+    "Expand code blocks by default": "Espandi blocchi di codice in modo predefinito",
+    "Show stickers button": "Mostra pulsante adesivi"
 }

From dbae13324c1ae9ba66b13e1b26aa16cf5acb4c9b Mon Sep 17 00:00:00 2001
From: Andrejs <tlpbu@droplar.com>
Date: Thu, 11 Feb 2021 20:24:53 +0000
Subject: [PATCH 138/389] Translated using Weblate (Latvian)

Currently translated at 47.7% (1321 of 2764 strings)

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

diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json
index a05bb81544..d294b38d82 100644
--- a/src/i18n/strings/lv.json
+++ b/src/i18n/strings/lv.json
@@ -166,7 +166,7 @@
     "No results": "Nav rezultātu",
     "No users have specific privileges in this room": "Šajā istabā nav lietotāju ar īpašām privilēģijām",
     "OK": "Labi",
-    "olm version:": "Olm versija:",
+    "olm version:": "olm versija:",
     "Only people who have been invited": "Vienīgi uzaicināti cilvēki",
     "Operation failed": "Darbība neizdevās",
     "Password": "Parole",
@@ -306,7 +306,7 @@
     "Tue": "O.",
     "Wed": "T.",
     "Thu": "C.",
-    "Fri": "P.",
+    "Fri": "Pk.",
     "Sat": "S.",
     "Jan": "Jan.",
     "Feb": "Feb.",
@@ -344,7 +344,7 @@
     "Import room keys": "Importēt istabas atslēgas",
     "File to import": "Importējamais fails",
     "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Šī darbība ļauj Tev uz lokālo failu eksportēt atslēgas priekš tām ziņām, kuras Tu saņēmi šifrētās istabās. Tu varēsi importēt šo failu citā Matrix klientā, lai tajā būtu iespējams lasīt šīs ziņas atšifrētas.",
-    "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.": "Eksportētā datne ļaus ikvienam, kurš to spēj to nolasīt, atšifrēt jebkuras jūsu šifrētās ziņas. Tādēļ ievērojiet piesardzību un glabājiet šo datni drošā vietā. Lai palīdzētu to nodrošināt, zemāk ievadiet frāzveida paroli eksportējamo datu šifrēšanai. Datu importēšana būs iespējama tikai izmantojot šo pašu frāzveida paroli.",
+    "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.": "Eksportētais fails ļaus ikvienam, kurš to spēj to nolasīt, atšifrēt jebkuras jūsu šifrētās ziņas. Tādēļ ievērojiet piesardzību un glabājiet šo failu drošā vietā. Lai palīdzētu to nodrošināt, zemāk ievadiet frāzveida paroli eksportējamo datu šifrēšanai. Datu importēšana būs iespējama tikai izmantojot šo pašu frāzveida paroli.",
     "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Šis process ļaus Tev importēt šifrēšanas atslēgas, kuras Tu iepriekš eksportēji no cita Matrix klienta. Tas ļaus Tev atšifrēt čata vēsturi.",
     "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Eksporta fails būs aizsargāts ar frāzveida paroli. Tā ir jāievada šeit, lai atšifrētu failu.",
     "You must join the room to see its files": "Tev ir jāpievienojas istabai, lai redzētu tās failus",
@@ -361,7 +361,7 @@
     "Please check your email to continue registration.": "Lūdzu pārbaudi savu epastu lai turpinātu reģistrāciju.",
     "Token incorrect": "Nepareizs autentifikācijas tokens",
     "Please enter the code it contains:": "Lūdzu, ievadiet tajā ietverto kodu:",
-    "powered by Matrix": "Tiek darbināta ar Matrix",
+    "powered by Matrix": "tiek darbināta ar Matrix",
     "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Ja Tu nenorādīsi epasta adresi, tev nebūs iespējams izmantot paroles atiestatīšanu. Vai to vēlies?",
     "Error decrypting audio": "Kļūda atšifrējot audio",
     "Error decrypting image": "Kļūda atšifrējot attēlu",
@@ -654,7 +654,7 @@
     "Uploading report": "Augšuplādē atskaiti",
     "Sunday": "Svētdiena",
     "Notification targets": "Paziņojumu adresāti",
-    "Today": "šodien",
+    "Today": "Šodien",
     "You are not receiving desktop notifications": "Darbvirsmas paziņojumi netiek saņemti",
     "Friday": "Piektdiena",
     "Update": "Atjaunināt",
@@ -976,7 +976,7 @@
     "Forgot password?": "Aizmirsi paroli?",
     "No homeserver URL provided": "Nav iestatīts bāzes servera URL",
     "Cannot reach homeserver": "Neizdodas savienoties ar bāzes serveri",
-    "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Datne '%(fileName)s pārsniedz augšupielādējamas datnes izmēra limitu šajā bāzes serverī",
+    "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Fails '%(fileName)s pārsniedz augšupielādējama faila izmēra limitu šajā bāzes serverī",
     "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Lūdzu, jautājiet sava bāzes servera administratoram (<code>%(homeserverDomain)s</code>) sakonfigurēt TURN serveri, lai zvani strādātu stabili.",
     "Join millions for free on the largest public server": "Pievienojieties bez maksas miljoniem lietotāju lielākajā publiskajā serverī",
     "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.": "Jūs varat pielāgot servera parametrus, lai pierakstītos citos Matrix bāzes serveros, norādot atbilstošu bāzes servera URL. Tas ļauj jums izmantot Element ar eksistējošu Matrix kontu uz cita bāzes servera.",
@@ -1185,7 +1185,7 @@
     "Room Settings - %(roomName)s": "Istabas iestatījumi - %(roomName)s",
     "Room settings": "Istabas iestatījumi",
     "Share room": "Dalīties ar istabu",
-    "Show files": "Rādīt datnes",
+    "Show files": "Rādīt failus",
     "Help & About": "Palīdzība un par lietotni",
     "About homeservers": "Par bāzes serveriem",
     "About": "Detaļas",

From 489d341d041fa65b696b18a41bc9d75947eaef57 Mon Sep 17 00:00:00 2001
From: Kaede <contact+element_translations@kaede.ch>
Date: Mon, 22 Feb 2021 00:28:34 +0000
Subject: [PATCH 139/389] Translated using Weblate (Japanese)

Currently translated at 50.5% (1398 of 2764 strings)

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

diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index 215921704d..bab9d94935 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -393,7 +393,7 @@
     "%(senderName)s sent a video": "%(senderName)s が動画を送信しました",
     "%(senderName)s uploaded a file": "%(senderName)s がファイルをアップロードしました",
     "Options": "オプション",
-    "Key request sent.": "キーリクエストが送信されました。",
+    "Key request sent.": "鍵リクエストが送信されました。",
     "Please select the destination room for this message": "このメッセージを送り先部屋を選択してください",
     "Disinvite": "招待拒否",
     "Kick": "追放する",
@@ -825,8 +825,8 @@
     "Account": "アカウント",
     "Access Token:": "アクセストークン:",
     "click to reveal": "クリックすると表示されます",
-    "Homeserver is": "ホームサーバーは",
-    "Identity Server is": "アイデンティティ・サーバー",
+    "Homeserver is": "ホームサーバー:",
+    "Identity Server is": "ID サーバー:",
     "%(brand)s version:": "%(brand)s のバージョン:",
     "olm version:": "olm のバージョン:",
     "Failed to send email": "メールを送信できませんでした",
@@ -1526,5 +1526,12 @@
     "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "まず、<existingIssuesLink>Github で既知のバグ</existingIssuesLink>を確認してください。また掲載されていない新しいバグを発見した場合は<newIssueLink>報告してください</newIssueLink>。",
     "Report a bug": "バグの報告",
     "Update %(brand)s": "%(brand)s の更新",
-    "New version of %(brand)s is available": "%(brand)s の新バージョンが利用可能です"
+    "New version of %(brand)s is available": "%(brand)s の新バージョンが利用可能です",
+    "If your other sessions do not have the key for this message you will not be able to decrypt them.": "あなたの他のセッションがこのメッセージの鍵を持っていない場合、このメッセージを復号することはできません。",
+    "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.": "鍵共有リクエストは自動的にあなたの他のセッションに送信されます。他のセッションで鍵共有リクエストを拒否または却下した場合は、ここをクリックしてこのセッションの鍵を再度リクエストしてください。",
+    "Your key share request has been sent - please check your other sessions for key share requests.": "鍵共有リクエストが送信されました。あなたの他のセッションで鍵共有リクエストをご確認ください。",
+    "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "あなたの他のセッションに<requestLink>暗号鍵を再リクエストする</requestLink>。",
+    "Block anyone not part of %(serverName)s from ever joining this room.": "%(serverName)s 以外からの参加をブロック",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "プライベートな部屋は招待者のみが参加できます。公開された部屋は誰でも検索・参加できます。",
+    "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Matrix 関連のセキュリティ問題を報告するには、Matrix.org の <a>Security Disclosure Policy</a> をご覧ください。"
 }

From ba12c274ce88bdb52273d3ff0144e0a7237df6de Mon Sep 17 00:00:00 2001
From: waclaw66 <waclaw66@seznam.cz>
Date: Thu, 18 Feb 2021 21:41:40 +0000
Subject: [PATCH 140/389] Translated using Weblate (Czech)

Currently translated at 100.0% (2764 of 2764 strings)

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

diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index 864c6208fa..3ee9c73399 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -771,7 +771,7 @@
     "System Alerts": "Systémová varování",
     "Muted Users": "Umlčení uživatelé",
     "Only room administrators will see this warning": "Toto varování uvidí jen správci místnosti",
-    "You don't currently have any stickerpacks enabled": "Momentálně nemáte aktívní žádné balíčky s nálepkami",
+    "You don't currently have any stickerpacks enabled": "Momentálně nemáte aktivní žádné balíčky s nálepkami",
     "Stickerpack": "Balíček s nálepkami",
     "Hide Stickers": "Skrýt nálepky",
     "Show Stickers": "Zobrazit nálepky",
@@ -1589,10 +1589,10 @@
     "Custom (%(level)s)": "Vlastní (%(level)s)",
     "Error upgrading room": "Chyba při upgrade místnosti",
     "Double check that your server supports the room version chosen and try again.": "Zkontrolujte, že váš server opravdu podporuje zvolenou verzi místnosti.",
-    "%(senderName)s placed a voice call.": "%(senderName)s spustil hovor.",
-    "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s spustil hovor. (není podporováno tímto prohlížečem)",
-    "%(senderName)s placed a video call.": "%(senderName)s spustil videohovor.",
-    "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s spustil videohovor. (není podporováno tímto prohlížečem)",
+    "%(senderName)s placed a voice call.": "%(senderName)s zahájil(a) hovor.",
+    "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s zahájil(a) hovor. (není podporováno tímto prohlížečem)",
+    "%(senderName)s placed a video call.": "%(senderName)s zahájil(a) videohovor.",
+    "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s zahájil(a) videohovor. (není podporováno tímto prohlížečem)",
     "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s odstranil(a) pravidlo blokující uživatele odpovídající %(glob)s",
     "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s odstranil pravidlo blokující místnosti odpovídající %(glob)s",
     "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s odstranil pravidlo blokující servery odpovídající %(glob)s",
@@ -2295,7 +2295,7 @@
     "Attach files from chat or just drag and drop them anywhere in a room.": "Připojte soubory z chatu nebo je jednoduše přetáhněte kamkoli do místnosti.",
     "No files visible in this room": "V této místnosti nejsou viditelné žádné soubory",
     "Show files": "Zobrazit soubory",
-    "%(count)s people|other": "%(count)s lidí",
+    "%(count)s people|other": "%(count)s osob",
     "About": "O",
     "You’re all caught up": "Vše vyřízeno",
     "You have no visible notifications in this room.": "V této místnosti nemáte žádná viditelná oznámení.",
@@ -2356,7 +2356,7 @@
     "delete the address.": "smazat adresu.",
     "Delete the room address %(alias)s and remove %(name)s from the directory?": "Smazat adresu místnosti %(alias)s a odebrat %(name)s z adresáře?",
     "Self-verification request": "Požadavek na sebeověření",
-    "%(creator)s created this DM.": "%(creator)s vytvořil tuto přímou zprávu.",
+    "%(creator)s created this DM.": "%(creator)s vytvořil(a) tuto přímou zprávu.",
     "You do not have permission to create rooms in this community.": "Nemáte oprávnění k vytváření místností v této skupině.",
     "Cannot create rooms in this community": "V této skupině nelze vytvořit místnosti",
     "Great, that'll help people know it's you": "Skvělé, to pomůže lidem zjistit, že jste to vy",
@@ -2644,7 +2644,7 @@
     "Enter name": "Zadejte jméno",
     "Ignored attempt to disable encryption": "Ignorovaný pokus o deaktivaci šifrování",
     "Show chat effects": "Zobrazit efekty chatu",
-    "%(count)s people|one": "%(count)s člověk",
+    "%(count)s people|one": "%(count)s osoba",
     "Takes the call in the current room off hold": "Zruší podržení hovoru v aktuální místnosti",
     "Places the call in the current room on hold": "Podrží hovor v aktuální místnosti",
     "Zimbabwe": "Zimbabwe",

From 997e9cc6b9932738cc9942171ee30d55cbfd6c51 Mon Sep 17 00:00:00 2001
From: Tuomas Hietala <tuomas.hietala@iki.fi>
Date: Mon, 15 Feb 2021 19:57:00 +0000
Subject: [PATCH 141/389] Translated using Weblate (Finnish)

Currently translated at 94.8% (2621 of 2764 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 | 46 +++++++++++++++++++++++++++-------------
 1 file changed, 31 insertions(+), 15 deletions(-)

diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json
index 1c518a681e..74fd5f5a9a 100644
--- a/src/i18n/strings/fi.json
+++ b/src/i18n/strings/fi.json
@@ -411,7 +411,7 @@
     "Unban this user?": "Poista tämän käyttäjän porttikielto?",
     "Ban this user?": "Anna porttikielto tälle käyttäjälle?",
     "Unignore": "Huomioi käyttäjä jälleen",
-    "Ignore": "Jätä käyttäjä huomioimatta",
+    "Ignore": "Sivuuta",
     "Jump to read receipt": "Hyppää lukukuittaukseen",
     "Mention": "Mainitse",
     "Invite": "Kutsu",
@@ -512,7 +512,7 @@
     "Please note you are logging into the %(hs)s server, not matrix.org.": "Huomaa että olet kirjautumassa palvelimelle %(hs)s, etkä palvelimelle matrix.org.",
     "Upload an avatar:": "Lataa profiilikuva:",
     "Deops user with given id": "Poistaa tunnuksen mukaiselta käyttäjältä ylläpito-oikeudet",
-    "Ignores a user, hiding their messages from you": "Jättää käyttäjän huomioimatta, jotta hänen viestejään ei näytetä sinulle",
+    "Ignores a user, hiding their messages from you": "Sivuuttaa käyttäjän, eli hänen viestejään ei näytetä sinulle",
     "Stops ignoring a user, showing their messages going forward": "Lopettaa käyttäjän huomiotta jättämisen, jotta hänen viestinsä näytetään sinulle",
     "Notify the whole room": "Ilmoita koko huoneelle",
     "Room Notification": "Huoneilmoitus",
@@ -521,7 +521,7 @@
     "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
     "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s %(day)s. %(monthName)s %(time)s",
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s %(day)s. %(monthName)s %(fullYear)s %(time)s",
-    "Ignored user": "Estetty käyttäjä",
+    "Ignored user": "Sivuutettu käyttäjä",
     "Unignored user": "Sallittu käyttäjä",
     "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s tasolta %(fromPowerLevel)s tasolle %(toPowerLevel)s",
     "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s muutti %(powerLevelDiffText)s:n oikeustasoa.",
@@ -1047,7 +1047,7 @@
     "Bug reporting": "Virheiden raportointi",
     "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Jos olet ilmoittanut virheestä Githubin kautta, debug-lokit voivat auttaa meitä ongelman jäljittämisessä. Debug-lokit sisältävät sovelluksen käyttödataa sisältäen käyttäjätunnuksen, vierailemiesi huoneiden tai ryhmien tunnukset tai aliakset ja muiden käyttäjien käyttäjätunnukset. Debug-lokit eivät sisällä viestejä.",
     "Autocomplete delay (ms)": "Automaattisen täydennyksen viive (ms)",
-    "Ignored users": "Hiljennetyt käyttäjät",
+    "Ignored users": "Sivuutetut käyttäjät",
     "Bulk options": "Massatoimintoasetukset",
     "Key backup": "Avainvarmuuskopio",
     "Missing media permissions, click the button below to request.": "Mediaoikeuksia puuttuu. Klikkaa painikkeesta pyytääksesi oikeuksia.",
@@ -1591,7 +1591,7 @@
     "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Voit käyttää identiteettipalvelinta lähettääksesi sähköpostikutsuja. Klikkaa Jatka käyttääksesi oletuspalvelinta (%(defaultIdentityServerName)s) tai syötä eri palvelin asetuksissa.",
     "Use an identity server to invite by email. Manage in Settings.": "Voit käyttää identiteettipalvelinta sähköpostikutsujen lähettämiseen.",
     "Multiple integration managers": "Useita integraatiolähteitä",
-    "Try out new ways to ignore people (experimental)": "Kokeile uusia tapoja käyttäjien huomiotta jättämiseen (kokeellinen)",
+    "Try out new ways to ignore people (experimental)": "Kokeile uusia tapoja käyttäjien sivuuttamiseen (kokeellinen)",
     "Match system theme": "Käytä järjestelmän teemaa",
     "Decline (%(counter)s)": "Hylkää (%(counter)s)",
     "Connecting to integration manager...": "Yhdistetään integraatioiden lähteeseen...",
@@ -1602,25 +1602,25 @@
     "Manage integrations": "Hallitse integraatioita",
     "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integraatioiden lähteet vastaanottavat asetusdataa ja voivat muokata sovelmia, lähettää kutsuja huoneeseen ja asettaa oikeustasoja puolestasi.",
     "Discovery": "Käyttäjien etsintä",
-    "Ignored/Blocked": "Jätetty huomiotta/estetty",
-    "Error adding ignored user/server": "Ongelma huomiotta jätetyn käyttäjän/palvelimen lisäämisessä",
+    "Ignored/Blocked": "Sivuutettu/estetty",
+    "Error adding ignored user/server": "Virhe sivuutetun käyttäjän/palvelimen lisäämisessä",
     "Error subscribing to list": "Virhe listalle liityttäessä",
-    "Error removing ignored user/server": "Ongelma huomiotta jätetyn käyttäjän/palvelimen poistamisessa",
+    "Error removing ignored user/server": "Virhe sivuutetun käyttäjän/palvelimen poistamisessa",
     "Error unsubscribing from list": "Virhe listalta poistuttaessa",
     "Ban list rules - %(roomName)s": "Estolistan säännöt - %(roomName)s",
     "Server rules": "Palvelinehdot",
     "User rules": "Käyttäjäehdot",
-    "You have not ignored anyone.": "Et ole jättänyt ketään huomiotta.",
+    "You have not ignored anyone.": "Et ole sivuuttanut ketään.",
     "You are currently ignoring:": "Jätät tällä hetkellä huomiotta:",
     "You are not subscribed to any lists": "Et ole liittynyt yhteenkään listaan",
     "You are currently subscribed to:": "Olet tällä hetkellä liittynyt:",
     "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Käyttäjien huomiotta jättäminen tapahtuu estolistojen kautta, joissa on tieto siitä, kenet pitää estää. Estolistalle liittyminen tarkoittaa, että ne käyttäjät/palvelimet, jotka tämä lista estää, eivät näy sinulle.",
     "Personal ban list": "Henkilökohtainen estolista",
     "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Henkilökohtainen estolistasi sisältää kaikki käyttäjät/palvelimet, joilta et henkilökohtaisesti halua nähdä viestejä. Sen jälkeen, kun olet estänyt ensimmäisen käyttäjän/palvelimen, huonelistaan ilmestyy uusi huone nimeltä ”Tekemäni estot” (englanniksi ”My Ban List”). Pysy tässä huoneessa, jotta estolistasi pysyy voimassa.",
-    "Server or user ID to ignore": "Huomiotta jätettävä palvelin tai käyttäjätunnus",
+    "Server or user ID to ignore": "Sivuutettava palvelin tai käyttäjätunnus",
     "Subscribed lists": "Tilatut listat",
     "Subscribing to a ban list will cause you to join it!": "Estolistan käyttäminen saa sinut liittymään listalle!",
-    "If this isn't what you want, please use a different tool to ignore users.": "Jos tämä ei ole mitä haluat, käytä eri työkalua käyttäjien huomiotta jättämiseen.",
+    "If this isn't what you want, please use a different tool to ignore users.": "Jos et halua tätä, käytä eri työkalua käyttäjien sivuuttamiseen.",
     "Integration Manager": "Integraatioiden lähde",
     "Read Marker lifetime (ms)": "Viestin luetuksi merkkaamisen kesto (ms)",
     "Click the link in the email you received to verify and then click continue again.": "Klikkaa lähettämässämme sähköpostissa olevaa linkkiä vahvistaaksesi tunnuksesi. Klikkaa sen jälkeen tällä sivulla olevaa painiketta ”Jatka”.",
@@ -1636,7 +1636,7 @@
     "Messages in this room are not end-to-end encrypted.": "Tämän huoneen viestit eivät ole päästä päähän -salattuja.",
     "Messages in this room are end-to-end encrypted.": "Tämän huoneen viestit ovat päästä päähän -salattuja.",
     "Verify": "Varmenna",
-    "You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "Olet jättänyt tämän käyttäjän huomiotta, joten hänen viestit ovat piilotettu. <a>Näytä käyttäjän viestit.</a>",
+    "You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "Olet sivuuttanut tämän käyttäjän, joten hänen viestinsä on piilotettu. <a>Näytä silti.</a>",
     "You verified %(name)s": "Varmensit käyttäjän %(name)s",
     "You cancelled verifying %(name)s": "Peruutit käyttäjän %(name)s varmennuksen",
     "%(name)s cancelled verifying": "%(name)s peruutti varmennuksen",
@@ -2054,7 +2054,7 @@
     "Encrypted by an unverified session": "Salattu varmentamattoman istunnon toimesta",
     "Encrypted by a deleted session": "Salattu poistetun istunnon toimesta",
     "Create room": "Luo huone",
-    "Reject & Ignore user": "Hylkää ja jätä käyttäjä huomiotta",
+    "Reject & Ignore user": "Hylkää ja sivuuta käyttäjä",
     "Start Verification": "Aloita varmennus",
     "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Viestisi ovat turvattu, ja vain sinulla ja vastaanottajalla on avaimet viestien lukemiseen.",
     "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "Salausta käyttävissä huoneissa viestisi on turvattu, ja vain sinulla ja vastaanottajilla on yksityiset avaimet viestien lukemiseen.",
@@ -2737,7 +2737,7 @@
     "Show message previews for reactions in DMs": "Näytä reaktioille esikatselu yksityisviesteissä",
     "Show message previews for reactions in all rooms": "Näytä reaktioille esikatselu kaikissa huoneissa",
     "New spinner design": "Uusi kehrääjä tyyli",
-    "Render LaTeX maths in messages": "Suorita LaTeX-matematiikkaa viesteissä",
+    "Render LaTeX maths in messages": "Piirrä LaTeX-matematiikka viesteissä",
     "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s",
     "Downloading logs": "Ladataan lokeja",
     "Uploading logs": "Lähetetään lokeja",
@@ -2854,5 +2854,21 @@
     "Converts the room to a DM": "Muuntaa huoneen yksityisviestiksi",
     "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Kotipalvelimesi hylkäsi kirjautumisyrityksesi. Syynä saattaa olla, että asiat tapahtuvat liian hitaasti. Yritä uudelleen. Mikäli ongelma jatkuu, ota yhteyttä kotipalvelimesi ylläpitäjään.",
     "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Kotipalvelintasi ei tavoitettu eikä sinua siksi kirjattu sisään. Yritä uudelleen. Mikäli ongelma jatkuu, ota yhteyttä kotipalvelimesi ylläpitäjään.",
-    "Try again": "Yritä uudelleen"
+    "Try again": "Yritä uudelleen",
+    "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Lisää tähän käyttäjät ja palvelimet, jotka haluat sivuuttaa. Asteriski täsmää mihin tahansa merkkiin. Esimerkiksi <code>@bot:*</code> sivuuttaa kaikki käyttäjät, joiden nimessä on \"bot\".",
+    "Show stickers button": "Näytä tarrapainike",
+    "Expand code blocks by default": "Laajenna koodilohkot oletuksena",
+    "Show line numbers in code blocks": "Näytä rivinumerot koodilohkoissa",
+    "Recently visited rooms": "Hiljattain vieraillut huoneet",
+    "Screens": "Näytöt",
+    "Share your screen": "Jaa näyttösi",
+    "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.": "Voit kirjautua muille Matrix-palvelimille antamalla eri kotipalvelimen URL-osoitteen palvelinasetuksissa. Näin voit käyttää Elementiä eri kotipalvelimella olevan Matrix-tilin kanssa.",
+    "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Voi olla paikallaan poistaa tämä käytöstä, jos huonetta käyttävät myös ulkoiset tiimit joilla on oma kotipalvelimensa. Asetusta ei voi muuttaa myöhemmin.",
+    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Voi olla paikallaan ottaa tämä käyttöön, jos huonetta käyttävät vain sisäiset tiimit kotipalvelimellasi. Asetusta ei voi muuttaa myöhemmin.",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Lue lisää: <privacyPolicyLink />, <termsOfServiceLink /> ja <cookiePolicyLink />.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Kotipalvelimeesi yhdistäminen ei onnistunut. Sulje tämä ikkuna ja yritä uudelleen.",
+    "Privacy Policy": "Tietosuojakäytäntö",
+    "Cookie Policy": "Evästekäytäntö",
+    "Recent changes that have not yet been received": "Tuoreet muutokset, joita ei ole vielä otettu vastaan",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Pyysimme selainta muistamaan kirjautumista varten mitä kotipalvelinta käytät, mutta selain on unohtanut sen. Mene kirjautumissivulle ja yritä uudelleen."
 }

From 9fc8d30550c44738c293c10bf841f9397b9843c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <sedivy.miro@gmail.com>
Date: Fri, 12 Feb 2021 19:35:15 +0000
Subject: [PATCH 142/389] Translated using Weblate (Slovak)

Currently translated at 65.5% (1813 of 2764 strings)

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

diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json
index 454d86cb64..0ee0c6cbc3 100644
--- a/src/i18n/strings/sk.json
+++ b/src/i18n/strings/sk.json
@@ -548,7 +548,7 @@
     "The phone number entered looks invalid": "Zdá sa, že zadané telefónne číslo je neplatné",
     "Error: Problem communicating with the given homeserver.": "Chyba: Nie je možné komunikovať so zadaným domovským serverom.",
     "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>.": "K domovskému serveru nie je možné pripojiť sa použitím protokolu HTTP keďže v adresnom riadku prehliadača máte HTTPS adresu. Použite protokol HTTPS alebo <a>povolte nezabezpečené skripty</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.": "Nie je možné pripojiť sa k domovskému serveru - skontrolujte prosím funkčnosť vašeho pripojenia na internet, uistite sa že <a>certifikát  domovského servera</a> je dôverihodný, a že žiaden doplnok nainštalovaný v prehliadači nemôže blokovať požiadavky.",
+    "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.": "Nie je možné pripojiť sa k domovskému serveru - skontrolujte prosím funkčnosť vášho pripojenia na internet. Uistite sa že <a>certifikát domovského servera</a> je dôveryhodný a že žiaden doplnok nainštalovaný v prehliadači nemôže blokovať požiadavky.",
     "Failed to fetch avatar URL": "Nepodarilo sa získať URL adresu obrázka",
     "Set a display name:": "Nastaviť zobrazované meno:",
     "Upload an avatar:": "Nahrať obrázok:",
@@ -573,7 +573,7 @@
     "Passphrases must match": "Heslá sa musia zhodovať",
     "Passphrase must not be empty": "Heslo nesmie byť prázdne",
     "Export room keys": "Exportovať kľúče miestností",
-    "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Tento proces vás prevedie exportom kľúčov určených na dešifrovanie správ, ktoré ste dostali v šifrovaných miestnostiach do lokálneho súboru. Tieto kľúče zo súboru môžete  neskôr importovať do iného Matrix klienta, aby ste v ňom mohli dešifrovať vaše šifrované správy.",
+    "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Tento proces vás prevedie exportom kľúčov určených na dešifrovanie správ, ktoré ste dostali v šifrovaných miestnostiach do lokálneho súboru. Tieto kľúče zo súboru môžete neskôr importovať do iného Matrix klienta, aby ste v ňom mohli dešifrovať vaše šifrované správy.",
     "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.": "Tento súbor umožní komukoľvek, kto má ku nemu prístup, dešifrovať všetky vami viditeľné šifrované správy, mali by ste teda byť opatrní a tento súbor si bezpečne uchovať. Aby bolo toto pre vás jednoduchšie, nižšie zadajte heslo, ktorým budú údaje v súbore zašifrované. Importovať údaje zo súboru bude možné len po zadaní tohoto istého hesla.",
     "Enter passphrase": "Zadajte (dlhé) heslo",
     "Confirm passphrase": "Potvrďte heslo",
@@ -613,7 +613,7 @@
     "This room is not showing flair for any communities": "V tejto miestnosti nie je zobrazená príslušnosť k žiadnym komunitám",
     "Display your community flair in rooms configured to show it.": "Zobrazovať vašu príslušnosť ku komunite v miestnostiach, ktoré sú nastavené na zobrazovanie tejto príslušnosti.",
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s",
-    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Túto zmenu nebudete môcť vrátiť späť pretože  znižujete vašu vlastnú úroveň moci. Ak ste jediný poverený používateľ v miestnosti, nebudete môcť znovu získať úroveň, akú máte teraz.",
+    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Túto zmenu nebudete môcť vrátiť späť, pretože znižujete vašu vlastnú úroveň moci. Ak ste jediný privilegovaný používateľ v miestnosti, nebudete môcť získať vašu súčasnú úroveň znovu.",
     "Send an encrypted reply…": "Odoslať šifrovanú odpoveď…",
     "Send an encrypted message…": "Odoslať šifrovanú správu…",
     "Replying": "Odpoveď",
@@ -727,7 +727,7 @@
     "You must specify an event type!": "Musíte nastaviť typ udalosti!",
     "(HTTP status %(httpStatus)s)": "(HTTP status %(httpStatus)s)",
     "All Rooms": "Vo všetkych miestnostiach",
-    "State Key": "State Key",
+    "State Key": "Stavový kľúč",
     "Wednesday": "Streda",
     "Quote": "Citovať",
     "Send logs": "Zahrnúť záznamy",
@@ -963,7 +963,7 @@
     "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Ak ste si nenastavili nový spôsob obnovenia, je možné, že útočník sa pokúša dostať k vášmu účtu. Radšej si ihneď zmeňte vaše heslo a nastavte si nový spôsob obnovenia v Nastaveniach.",
     "Set up Secure Messages": "Nastaviť bezpečné obnovenie správ",
     "Go to Settings": "Otvoriť nastavenia",
-    "Whether or not you're logged in (we don't record your username)": "Či ste alebo nie ste prihlásení (nezaznamenávame vaše meno používateľa)",
+    "Whether or not you're logged in (we don't record your username)": "Či ste alebo nie ste prihlásení (nezaznamenávame vaše používateľské meno)",
     "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Veľkosť súboru „%(fileName)s“ prekračuje limit veľkosti súboru nahrávania na tento domovský server",
     "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Pridá znaky ¯\\_(ツ)_/¯ pred správy vo formáte obyčajného textu",
     "Upgrades a room to a new version": "Aktualizuje miestnosť na novšiu verziu",
@@ -1289,7 +1289,7 @@
     "Double check that your server supports the room version chosen and try again.": "Uistite sa, že domovský server podporuje zvolenú verziu miestnosti a skúste znovu.",
     "Changes the avatar of the current room": "Zmení obrázok miestnosti",
     "Use an identity server": "Použiť server totožností",
-    "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Aby ste mohli používateľov pozývať zadaním emailovej adresy, je potrebné nastaviť adresu servera totožností. Klepnutím na tlačidlo pokračovať  použijete predvolený server (%(defaultIdentityServerName)s) a zmeniť to môžete v nastaveniach.",
+    "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Aby ste mohli používateľov pozývať zadaním emailovej adresy, je potrebné nastaviť adresu servera totožností. Klepnutím na tlačidlo pokračovať použijete predvolený server (%(defaultIdentityServerName)s) a zmeniť to môžete v nastaveniach.",
     "Use an identity server to invite by email. Manage in Settings.": "Server totožností sa použije na pozývanie používateľov zadaním emailovej adresy. Spravujte v nastaveniach.",
     "%(senderName)s placed a voice call.": "%(senderName)s uskutočnil telefonát.",
     "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s uskutočnil telefonát. (Nepodporované týmto prehliadačom)",
@@ -1371,27 +1371,27 @@
     "Discovery": "Objaviť",
     "Deactivate account": "Deaktivovať účet",
     "Clear cache and reload": "Vymazať vyrovnávaciu pamäť a načítať znovu",
-    "Customise your experience with experimental labs features. <a>Learn more</a>.": "Prispôsobte si zážitok s používania  aktivovaním experimentálnych vlastností. <a>Zistiť viac</a>.",
+    "Customise your experience with experimental labs features. <a>Learn more</a>.": "Prispôsobte si zážitok z používania aktivovaním experimentálnych vlastností. <a>Zistiť viac</a>.",
     "Ignored/Blocked": "Ignorovaní / Blokovaní",
     "Error adding ignored user/server": "Chyba pri pridávaní ignorovaného používateľa / servera",
     "Something went wrong. Please try again or view your console for hints.": "Niečo sa nepodarilo. Prosím, skúste znovu neskôr alebo si prečítajte ďalšie usmernenia zobrazením konzoly.",
     "Error subscribing to list": "Chyba pri prihlasovaní sa do zoznamu",
     "Error removing ignored user/server": "Chyba pri odstraňovaní ignorovaného používateľa / servera",
-    "Use Single Sign On to continue": "Pokračovať pomocou Jednotného prihlásenia",
-    "Confirm adding this email address by using Single Sign On to prove your identity.": "Potvrďte pridanie tejto adresy pomocou Jednotného prihlásenia.",
-    "Single Sign On": "Jednotné prihlásenie",
+    "Use Single Sign On to continue": "Pokračovať pomocou Single Sign On",
+    "Confirm adding this email address by using Single Sign On to prove your identity.": "Potvrďte pridanie tejto adresy pomocou Single Sign On.",
+    "Single Sign On": "Single Sign On",
     "Confirm adding email": "Potvrdiť pridanie emailu",
-    "Click the button below to confirm adding this email address.": "Kliknutím na tlačítko potvrdíte pridanie emailovej adresy.",
-    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Potvrďte pridanie telefónneho čísla pomocou Jednotného prihlásenia.",
+    "Click the button below to confirm adding this email address.": "Kliknutím na tlačidlo nižšie potvrdíte pridanie emailovej adresy.",
+    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Potvrďte pridanie telefónneho čísla pomocou Single Sign On.",
     "Confirm adding phone number": "Potvrdiť pridanie telefónneho čísla",
-    "Click the button below to confirm adding this phone number.": "Kliknutím na tlačítko potvrdíte pridanie telefónneho čísla.",
+    "Click the button below to confirm adding this phone number.": "Kliknutím na tlačidlo nižšie potvrdíte pridanie telefónneho čísla.",
     "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Či používate %(brand)s na zariadení, ktorého hlavným vstupným mechanizmom je dotyk (mobil, tablet,...)",
     "Whether you're using %(brand)s as an installed Progressive Web App": "Či používate %(brand)s ako nainštalovanú Progresívnu Webovú Aplikáciu",
     "Your user agent": "Identifikátor vášho prehliadača",
     "If you cancel now, you won't complete verifying the other user.": "Pokiaľ teraz proces zrušíte, nedokončíte overenie druhého používateľa.",
     "If you cancel now, you won't complete verifying your other session.": "Pokiaľ teraz proces zrušíte, nedokončíte overenie vašej druhej relácie.",
     "If you cancel now, you won't complete your operation.": "Pokiaľ teraz proces zrušíte, nedokončíte ho.",
-    "Cancel entering passphrase?": "Zrušiť zadávanie (dlhého) hesla.",
+    "Cancel entering passphrase?": "Želáte si zrušiť zadávanie hesla?",
     "Setting up keys": "Príprava kľúčov",
     "Verify this session": "Overiť túto reláciu",
     "Enter recovery passphrase": "Zadajte (dlhé) heslo pre obnovu zálohy",
@@ -1439,7 +1439,7 @@
     "Interactively verify by Emoji": "Interaktívne overte pomocou emoji",
     "Done": "Hotovo",
     "a few seconds ago": "pred pár sekundami",
-    "about a minute ago": "približne pred minutou",
+    "about a minute ago": "približne pred minútou",
     "about an hour ago": "približne pred hodinou",
     "about a day ago": "približne deň dozadu",
     "a few seconds from now": "o pár sekúnd",
@@ -1474,7 +1474,7 @@
     "Failed to re-authenticate": "Opätovná autentifikácia zlyhala",
     "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Znovuzískajte prístup k vášmu účtu a obnovte šifrovacie kľúče uložené v tejto relácií. Bez nich nebudete môcť čítať všetky vaše šifrované správy vo všetkých reláciach.",
     "Font scaling": "Škálovanie písma",
-    "Show info about bridges in room settings": "Zobraziť informácie o mostoch v Nastaveniach miestnosti",
+    "Show info about bridges in room settings": "Zobraziť informácie o mostoch v nastaveniach miestnosti",
     "Font size": "Veľkosť písma",
     "Show typing notifications": "Posielať oznámenia, keď píšete",
     "Never send encrypted messages to unverified sessions from this session": "Nikdy neposielať šifrované správy neovereným reláciam z tejto relácie",
@@ -1519,7 +1519,7 @@
     "To be secure, do this in person or use a trusted way to communicate.": "Aby ste si boli istý, urobte to osobne alebo použite dôveryhodný spôsob komunikácie.",
     "Lock": "Zámok",
     "If you can't scan the code above, verify by comparing unique emoji.": "Pokiaľ nemôžete kód vyššie skenovať, overte sa porovnaním jedinečnej kombinácie emoji.",
-    "Verify by comparing unique emoji.": "Overenie porovnaním jedinečnej kombinácie emoji",
+    "Verify by comparing unique emoji.": "Overenie porovnaním jedinečnej kombinácie emotikonov.",
     "Verify by emoji": "Overte pomocou emoji",
     "Compare emoji": "Porovnajte emoji",
     "Verify all your sessions to ensure your account & messages are safe": "Overte všetky vaše relácie, aby ste si boli istý, že sú vaše správy a účet bezpečné",
@@ -1528,7 +1528,7 @@
     "Upgrade": "Upgradovať",
     "Verify": "Overiť",
     "Verify yourself & others to keep your chats safe": "Overte seba a ostatných, aby vaše komunikácie boli bezpečné",
-    "Other users may not trust it": "Ostatný používatelia jej nemusia veriť",
+    "Other users may not trust it": "Ostatní používatelia jej nemusia veriť",
     "Verify the new login accessing your account: %(name)s": "Overte nové prihlásenie na váš účet: %(name)s",
     "From %(deviceName)s (%(deviceId)s)": "Od %(deviceName)s (%(deviceId)s)",
     "This bridge was provisioned by <user />.": "Tento most poskytuje <user />.",
@@ -1545,7 +1545,7 @@
     "unexpected type": "neočakávaný typ",
     "in memory": "v pamäti",
     "Self signing private key:": "Samo-podpísané súkromné kľúče:",
-    "cached locally": "cachenuté lokálne",
+    "cached locally": "uložené do lokálnej vyrovnávacej pamäťe",
     "not found locally": "nenájdené lokálne",
     "User signing private key:": "Používateľom podpísané súkromné kľúče:",
     "Session backup key:": "Kľúč na zálohu relácie:",
@@ -1595,7 +1595,7 @@
     "Restart": "Reštartovať",
     "Upgrade your %(brand)s": "Upgradujte svoj %(brand)s",
     "A new version of %(brand)s is available!": "Nová verzia %(brand)su je dostupná!",
-    "Which officially provided instance you are using, if any": "Ktorú oficiálne poskytovanú inštanciu používate, pokiaľ nejakú",
+    "Which officially provided instance you are using, if any": "Ktorú oficiálne poskytovanú inštanciu používate, ak nejakú",
     "Use your account to sign in to the latest version": "Použite svoj účet na prihlásenie sa do najnovšej verzie",
     "We’re excited to announce Riot is now Element": "Sme nadšený oznámiť, že Riot je odteraz Element",
     "Riot is now Element!": "Riot je odteraz Element!",
@@ -1669,7 +1669,7 @@
     "Incoming video call": "Prichádzajúci video hovor",
     "Incoming call": "Prichádzajúci hovor",
     "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Vytvorte si filter, potiahnite avatara komunity do panelu filtrov na ľavý okraj obrazovky. Môžete kliknúť na avatara v paneli filtorv, aby ste videli len miestnosti a ľudí patriacich do danej komunity.",
-    "%(num)s minutes ago": "pred %(num)s minútami",
+    "%(num)s minutes ago": "pred %(num)s min",
     "%(num)s hours ago": "pred %(num)s hodinami",
     "%(num)s days ago": "pred %(num)s dňami",
     "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s nemôže bezpečne cachovať šiforvané správy lokálne pomocou prehlliadača. Použite <desktopLink>%(brand)s Desktop</desktopLink> na zobrazenie výsledkov vyhľadávania šiforavných správ.",
@@ -1775,10 +1775,10 @@
     "%(num)s hours from now": "o %(num)s hodín",
     "%(num)s days from now": "o %(num)s dní",
     "The person who invited you already left the room.": "Osoba ktorá Vás pozvala už opustila miestnosť.",
-    "The person who invited you already left the room, or their server is offline.": "Osoba ktorá Vás pozvala už opustila miestnosť, alebo je ich server offline.",
+    "The person who invited you already left the room, or their server is offline.": "Osoba, ktorá Vás pozvala už opustila miestnosť, alebo je jej server offline.",
     "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s",
     "Change notification settings": "Upraviť nastavenia upozornení",
-    "Enable advanced debugging for the room list": "Zapnúť pokročilé ladenie pre zoznam miestností",
+    "Enable advanced debugging for the room list": "Zapnúť pokročilé nástroje ladenia pre zoznam miestností",
     "Securely cache encrypted messages locally for them to appear in search results, using ": "Bezpečne uchovávať šifrované správy na tomto zariadení, aby sa v nich dalo vyhľadávať pomocou ",
     " to store messages from ": " na uchovanie správ z ",
     "rooms.": "miestnosti.",
@@ -1811,5 +1811,274 @@
     "Security": "Zabezpečenie",
     "Send a Direct Message": "Poslať priamu správu",
     "User menu": "Používateľské menu",
-    "Toggle Italics": "Prepnúť kurzíva"
+    "Toggle Italics": "Prepnúť kurzíva",
+    "Zimbabwe": "Zimbabwe",
+    "Zambia": "Zambia",
+    "Yemen": "Jemen",
+    "Western Sahara": "Západná Sahara",
+    "Wallis & Futuna": "Wallis a Futuna",
+    "Vietnam": "Vietnam",
+    "Venezuela": "Venezuela",
+    "Vatican City": "Vatikán",
+    "Vanuatu": "Vanuatu",
+    "Uzbekistan": "Uzbekistan",
+    "Uruguay": "Uruguaj",
+    "United Arab Emirates": "Spojené Arabské Emiráty",
+    "Ukraine": "Ukrajina",
+    "Uganda": "Uganda",
+    "U.S. Virgin Islands": "Americké Panenské ostrovy",
+    "Tuvalu": "Tuvalu",
+    "Turks & Caicos Islands": "Ostrovy Turks a Caicos",
+    "Turkmenistan": "Turkménsko",
+    "Turkey": "Turecko",
+    "Tunisia": "Tunisko",
+    "Trinidad & Tobago": "Trinidad a Tobago",
+    "Tonga": "Tonga",
+    "Tokelau": "Tokelau",
+    "Togo": "Togo",
+    "Timor-Leste": "Východný Timor",
+    "Thailand": "Thajsko",
+    "Tanzania": "Tanzánia",
+    "Tajikistan": "Tadžikistan",
+    "Taiwan": "Taiwan",
+    "São Tomé & Príncipe": "Svätý Tomáš a Princov ostrov",
+    "Syria": "Sýria",
+    "Switzerland": "Švajčiarsko",
+    "Sweden": "Švédsko",
+    "Swaziland": "Svazijsko",
+    "Svalbard & Jan Mayen": "Špicbergy a Jan Mayen",
+    "Suriname": "Surinam",
+    "Sudan": "Sudán",
+    "St. Vincent & Grenadines": "Svätý Vincent a Grenadíny",
+    "St. Pierre & Miquelon": "Svätý Pierre a Miquelon",
+    "St. Martin": "Svätý Martin",
+    "St. Lucia": "Svätá Lucia",
+    "St. Kitts & Nevis": "Svätý Krištof a Nevis",
+    "St. Helena": "Svätá Helena",
+    "St. Barthélemy": "Svätý Bartolomej",
+    "Sri Lanka": "Srí Lanka",
+    "Spain": "Španielsko",
+    "South Sudan": "Južný Sudán",
+    "South Korea": "Južná Kórea",
+    "South Georgia & South Sandwich Islands": "Južná Georgia a Južné sendvičové ostrovy",
+    "South Africa": "Južná Afrika",
+    "Somalia": "Somálsko",
+    "Solomon Islands": "Šalamúnove ostrovy",
+    "Slovenia": "Slovinsko",
+    "Slovakia": "Slovensko",
+    "Sint Maarten": "Sint Maarten",
+    "Singapore": "Singapur",
+    "Sierra Leone": "Sierra Leone",
+    "Seychelles": "Seychely",
+    "Serbia": "Srbsko",
+    "Senegal": "Senegal",
+    "Saudi Arabia": "Saudská Arábia",
+    "San Marino": "San Maríno",
+    "Samoa": "Samoa",
+    "Réunion": "Réunion",
+    "Rwanda": "Rwanda",
+    "Russia": "Rusko",
+    "Romania": "Rumunsko",
+    "Qatar": "Katar",
+    "Puerto Rico": "Portoriko",
+    "Portugal": "Portugalsko",
+    "Poland": "Poľsko",
+    "Pitcairn Islands": "Pitcairnove ostrovy",
+    "Philippines": "Filipíny",
+    "Peru": "Peru",
+    "Paraguay": "Paraguaj",
+    "Papua New Guinea": "Papua-Nová Guinea",
+    "Panama": "Panama",
+    "Palestine": "Palestína",
+    "Palau": "Palau",
+    "Pakistan": "Pakistan",
+    "Oman": "Omán",
+    "Norway": "Nórsko",
+    "Northern Mariana Islands": "Severné Mariány",
+    "North Korea": "Severná Kórea",
+    "Norfolk Island": "Ostrov Norfolk",
+    "Niue": "Niue",
+    "Nigeria": "Nigéria",
+    "Niger": "Niger",
+    "Nicaragua": "Nikaragua",
+    "New Zealand": "Nový Zéland",
+    "New Caledonia": "Nová Kaledónia",
+    "Netherlands": "Holandsko",
+    "Nepal": "Nepál",
+    "Nauru": "Nauru",
+    "Namibia": "Namíbia",
+    "Myanmar": "Mjanmarsko",
+    "Mozambique": "Mozambik",
+    "Morocco": "Maroko",
+    "Montserrat": "Montserrat",
+    "Montenegro": "Čierna Hora",
+    "Mongolia": "Mongolsko",
+    "Monaco": "Monako",
+    "Moldova": "Moldavsko",
+    "Micronesia": "Mikronézia",
+    "Mexico": "Mexiko",
+    "Mayotte": "Mayotte",
+    "Mauritius": "Maurícius",
+    "Mauritania": "Mauretánia",
+    "Martinique": "Martinik",
+    "Marshall Islands": "Maršalove ostrovy",
+    "Malta": "Malta",
+    "Mali": "Mali",
+    "Maldives": "Maledivy",
+    "Malaysia": "Malajzia",
+    "Malawi": "Malawi",
+    "Madagascar": "Madagaskar",
+    "Macedonia": "Macedónsko",
+    "Macau": "Macao",
+    "Luxembourg": "Luxembursko",
+    "Lithuania": "Litva",
+    "Liechtenstein": "Lichtenštajnsko",
+    "Libya": "Líbya",
+    "Liberia": "Libéria",
+    "Lesotho": "Lesotho",
+    "Lebanon": "Libanon",
+    "Latvia": "Lotyšsko",
+    "Laos": "Laos",
+    "Kyrgyzstan": "Kirgizsko",
+    "Kuwait": "Kuvajt",
+    "Kosovo": "Kosovo",
+    "Kiribati": "Kiribati",
+    "Kenya": "Keňa",
+    "Kazakhstan": "Kazachstan",
+    "Jordan": "Jordánsko",
+    "Jersey": "Jersey",
+    "Japan": "Japonsko",
+    "Jamaica": "Jamajka",
+    "Italy": "Taliansko",
+    "Israel": "Izrael",
+    "Isle of Man": "Ostrov Man",
+    "Ireland": "Írsko",
+    "Iraq": "Irak",
+    "Iran": "Irán",
+    "Indonesia": "Indonézia",
+    "India": "India",
+    "Iceland": "Island",
+    "Hungary": "Maďarsko",
+    "Hong Kong": "Hongkong",
+    "Honduras": "Honduras",
+    "Heard & McDonald Islands": "Teritórium Heardovho ostrova a Macdonaldových ostrovov",
+    "Haiti": "Haiti",
+    "Guyana": "Guyana",
+    "Guinea-Bissau": "Guinea-Bissau",
+    "Guinea": "Guinea",
+    "Guernsey": "Guernsey",
+    "Guatemala": "Guatemala",
+    "Guam": "Guam",
+    "Guadeloupe": "Guadeloupe",
+    "Grenada": "Grenada",
+    "Greenland": "Grónsko",
+    "Greece": "Grécko",
+    "Gibraltar": "Gibraltár",
+    "Ghana": "Ghana",
+    "Germany": "Nemecko",
+    "Georgia": "Gruzínsko",
+    "Gambia": "Gambia",
+    "Gabon": "Gabon",
+    "French Southern Territories": "Francúzske južné územia",
+    "French Polynesia": "Francúzska Polynézia",
+    "French Guiana": "Francúzska Guiana",
+    "France": "Francúzsko",
+    "Finland": "Fínsko",
+    "Fiji": "Fidži",
+    "Faroe Islands": "Faerské ostrovy",
+    "Falkland Islands": "Falklandské ostrovy",
+    "Ethiopia": "Etiópia",
+    "Estonia": "Estónsko",
+    "Eritrea": "Eritrea",
+    "Equatorial Guinea": "Rovníková Guinea",
+    "El Salvador": "El Salvador",
+    "Egypt": "Egypt",
+    "Ecuador": "Ekvádor",
+    "Dominican Republic": "Dominikánska republika",
+    "Dominica": "Dominika",
+    "Djibouti": "Džibuti",
+    "Denmark": "Dánsko",
+    "Côte d’Ivoire": "Pobrežie Slonoviny",
+    "Czech Republic": "Česká republika",
+    "Cyprus": "Cyprus",
+    "Curaçao": "Curaçao",
+    "Cuba": "Kuba",
+    "Croatia": "Chorvátsko",
+    "Costa Rica": "Kostarika",
+    "Cook Islands": "Cookove ostrovy",
+    "Congo - Kinshasa": "Kongo (Kinshasa)",
+    "Congo - Brazzaville": "Kongo (Brazzaville)",
+    "Comoros": "Komory",
+    "Colombia": "Kolumbia",
+    "Cocos (Keeling) Islands": "Kokosové ostrovy",
+    "Christmas Island": "Vianočný ostrov",
+    "China": "Čína",
+    "Chile": "Čile",
+    "Chad": "Čad",
+    "Central African Republic": "Stredoafrická republika",
+    "Cayman Islands": "Kajmanské ostrovy",
+    "Caribbean Netherlands": "Karibské Holandsko",
+    "Cape Verde": "Kapverdy",
+    "Canada": "Kanada",
+    "Cameroon": "Kamerun",
+    "Cambodia": "Kambodža",
+    "Burundi": "Burundi",
+    "Burkina Faso": "Burkina Faso",
+    "Bulgaria": "Bulharsko",
+    "Brunei": "Brunej",
+    "British Virgin Islands": "Britské Panenské ostrovy",
+    "British Indian Ocean Territory": "Britské indickooceánske územie",
+    "Brazil": "Brazília",
+    "Bouvet Island": "Bouvetov ostrov",
+    "Botswana": "Botswana",
+    "Bosnia": "Bosna",
+    "Bolivia": "Bolívia",
+    "Bhutan": "Bhután",
+    "Bermuda": "Bermudy",
+    "Benin": "Benin",
+    "Belize": "Belize",
+    "Belgium": "Belgicko",
+    "Belarus": "Bielorusko",
+    "Barbados": "Barbados",
+    "Bangladesh": "Bangladéš",
+    "Bahrain": "Bahrajn",
+    "Bahamas": "Bahamy",
+    "Azerbaijan": "Azerbajdžan",
+    "Austria": "Rakúsko",
+    "Australia": "Austrália",
+    "Aruba": "Aruba",
+    "Armenia": "Arménsko",
+    "Argentina": "Argentína",
+    "Antigua & Barbuda": "Antigua a Barbuda",
+    "Antarctica": "Antarktída",
+    "Anguilla": "Anguilla",
+    "Angola": "Angola",
+    "Andorra": "Andorra",
+    "American Samoa": "Americká Samoa",
+    "Algeria": "Alžírsko",
+    "Albania": "Albánsko",
+    "Åland Islands": "Alandské ostrovy",
+    "Afghanistan": "Afganistan",
+    "United States": "Spojené Štáty",
+    "United Kingdom": "Spojené Kráľovstvo",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Domáci server odmietol váš pokus o prihlásenie. Môže to byť spôsobené tým, že požiadavky trvajú príliš dlho. Prosím skúste znova. Ak to bude pokračovať, kontaktujte svojho správcu domovského servera.",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Váš domovský server bol nedosiahnuteľný a nemohol vás prihlásiť. Skúste to znova. Ak to bude pokračovať, kontaktujte svojho správcu domovského servera.",
+    "Try again": "Skúste to znova",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Požiadali sme prehliadač, aby si pamätal, aký domovský server používate na prihlásenie, ale váš prehliadač ho, bohužiaľ, zabudol. Prejdite na prihlasovaciu stránku a skúste to znova.",
+    "We couldn't log you in": "Nemohli sme vás prihlásiť",
+    "This will end the conference for everyone. Continue?": "Týmto sa konferencia ukončí pre všetkých. Chcete pokračovať?",
+    "End conference": "Koniec konferencie",
+    "You've reached the maximum number of simultaneous calls.": "Dosiahli ste maximálny počet súčasných hovorov.",
+    "Too Many Calls": "Príliš veľa hovorov",
+    "No other application is using the webcam": "Webkameru nepoužíva žiadna iná aplikácia",
+    "Permission is granted to use the webcam": "Udeľuje sa povolenie na používanie webkamery",
+    "A microphone and webcam are plugged in and set up correctly": "Mikrofón a webkamera sú pripojené a správne nastavené",
+    "Call failed because webcam or microphone could not be accessed. Check that:": "Hovor zlyhal, pretože nebolo možné získať prístup k webkamere alebo mikrofónu. Skontrolujte, či:",
+    "Unable to access webcam / microphone": "Nie je možné získať prístup k webkamere / mikrofónu",
+    "Unable to access microphone": "Nie je možné získať prístup k mikrofónu",
+    "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Hovor zlyhal, pretože nebolo možné získať prístup k mikrofónu. Skontrolujte, či je mikrofón pripojený a správne nastavený.",
+    "The call was answered on another device.": "Hovor bol prijatý na inom zariadení.",
+    "The call could not be established": "Hovor nemohol byť realizovaný",
+    "The other party declined the call.": "Druhá strana odmietla hovor.",
+    "Call Declined": "Hovor odmietnutý"
 }

From 134d18554a9ec899dc92d036c66b1492533d9b2b Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Fri, 12 Feb 2021 04:17:11 +0000
Subject: [PATCH 143/389] Translated using Weblate (Galician)

Currently translated at 100.0% (2764 of 2764 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 | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 006412fb88..b309f12245 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -3052,5 +3052,18 @@
     "Share your screen": "Compartir a túa pantalla",
     "Recently visited rooms": "Salas visitadas recentemente",
     "Show line numbers in code blocks": "Mostrar números de liña nos bloques de código",
-    "Expand code blocks by default": "Por omsión despregar bloques de código"
+    "Expand code blocks by default": "Por omsión despregar bloques de código",
+    "Upgrade to pro": "Mellorar a pro",
+    "Minimize dialog": "Minimizar ventá",
+    "Maximize dialog": "Maximizar ventá",
+    "%(hostSignupBrand)s Setup": "Configurar %(hostSignupBrand)s",
+    "You should know": "Deberías saber",
+    "Privacy Policy": "Política de Privacidade",
+    "Cookie Policy": "Política de Cookies",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Aprende máis na nosa <privacyPolicyLink />, <termsOfServiceLink /> e <cookiePolicyLink />.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Ao continuar de xeito temporal permitirás que %(hostSignupBrand)s complete o acceso á túa conta para obter os enderezos de email verificados. Os datos non se almacenan.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Fallou a conexión co teu servidor de inicio. Pecha esta información e inténtao outra vez.",
+    "Abort": "Abortar",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Tes a certeza de querer cancelar a creación do servidor? O proceso non pode ser completado.",
+    "Confirm abort of host creation": "Corfirma que cancelas a creación do servidor"
 }

From 041f74eaafb58f1ca8c95dbac5221b4499ba8813 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Slobodan=20Simi=C4=87?= <slsimic@gmail.com>
Date: Mon, 22 Feb 2021 08:36:41 +0000
Subject: [PATCH 144/389] Translated using Weblate (Serbian)

Currently translated at 50.6% (1401 of 2764 strings)

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

diff --git a/src/i18n/strings/sr.json b/src/i18n/strings/sr.json
index 3ca12d2fd1..ca79955f72 100644
--- a/src/i18n/strings/sr.json
+++ b/src/i18n/strings/sr.json
@@ -1001,7 +1001,7 @@
     "Show rooms with unread notifications first": "Прво прикажи собе са непрочитаним обавештењима",
     "Enable experimental, compact IRC style layout": "Омогући пробни, збијенији распоред у IRC стилу",
     "Got It": "Разумем",
-    "Light bulb": "Сијалица",
+    "Light bulb": "сијалица",
     "Algorithm: ": "Алгоритам: ",
     "Go back": "Назад",
     "Hey you. You're the best!": "Хеј! Само напред!",
@@ -1438,5 +1438,75 @@
     "Changes your display nickname in the current room only": "Мења ваше приказно име само у тренутној соби",
     "Try again": "Покушај поново",
     "We couldn't log you in": "Не могу да вас пријавим",
-    "Double check that your server supports the room version chosen and try again.": "Добро проверите да ли сервер подржава изабрану верзију собе и пробајте поново."
+    "Double check that your server supports the room version chosen and try again.": "Добро проверите да ли сервер подржава изабрану верзију собе и пробајте поново.",
+    "a few seconds from now": "за неколико секунди",
+    "The message you are trying to send is too large.": "Порука коју покушавате да пошаљете је предугачка.",
+    "Pin": "чиода",
+    "Folder": "фасцикла",
+    "Headphones": "слушалице",
+    "Anchor": "сидро",
+    "Bell": "звоно",
+    "Trumpet": "труба",
+    "Guitar": "гитара",
+    "Ball": "лопта",
+    "Trophy": "трофеј",
+    "Rocket": "ракета",
+    "Aeroplane": "авион",
+    "Bicycle": "бицикл",
+    "Train": "воз",
+    "Flag": "застава",
+    "Telephone": "телефон",
+    "Hammer": "чекић",
+    "Key": "кључ",
+    "Lock": "катанац",
+    "Scissors": "маказе",
+    "Paperclip": "спајалица",
+    "Pencil": "оловка",
+    "Book": "књига",
+    "Gift": "поклон",
+    "Clock": "сат",
+    "Hourglass": "пешчаник",
+    "Umbrella": "кишобран",
+    "Thumbs up": "палац горе",
+    "Santa": "Мраз",
+    "Spanner": "кључ",
+    "Glasses": "наочаре",
+    "Hat": "шешир",
+    "Robot": "робот",
+    "Smiley": "смајли",
+    "Heart": "срце",
+    "Cake": "торта",
+    "Pizza": "пица",
+    "Corn": "кукуруз",
+    "Strawberry": "јагода",
+    "Apple": "јабука",
+    "Banana": "банана",
+    "Fire": "ватра",
+    "Cloud": "облак",
+    "Moon": "месец",
+    "Globe": "глобус",
+    "Mushroom": "печурка",
+    "Cactus": "кактус",
+    "Tree": "дрво",
+    "Flower": "цвет",
+    "Butterfly": "лептир",
+    "Octopus": "октопод",
+    "Fish": "риба",
+    "Turtle": "корњача",
+    "Penguin": "пингвин",
+    "Rooster": "петао",
+    "Panda": "панда",
+    "Rabbit": "зец",
+    "Elephant": "слон",
+    "Pig": "прасе",
+    "Unicorn": "једнорог",
+    "Horse": "коњ",
+    "Lion": "лав",
+    "Cat": "мачка",
+    "Dog": "пас",
+    "To be secure, do this in person or use a trusted way to communicate.": "Да будете сигурни, ово обавите лично или путем поузданог начина комуникације.",
+    "They don't match": "Не поклапају се",
+    "They match": "Поклапају се",
+    "Cancelling…": "Отказујем…",
+    "Show stickers button": "Прикажи дугме за налепнице"
 }

From 03589af8ad3208cd88683d986685cdb457345d62 Mon Sep 17 00:00:00 2001
From: Fake Mail <fakemail123@abv.bg>
Date: Thu, 11 Feb 2021 22:13:10 +0000
Subject: [PATCH 145/389] Translated using Weblate (Bulgarian)

Currently translated at 93.3% (2581 of 2764 strings)

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

diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json
index e4010e8b64..8c6ea60a8d 100644
--- a/src/i18n/strings/bg.json
+++ b/src/i18n/strings/bg.json
@@ -851,7 +851,7 @@
     "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Съобщението Ви не бе изпратено, защото този сървър е някой от лимитите си. Моля, <a>свържете се с администратора на услугата</a> за да продължите да я използвате.",
     "Please <a>contact your service administrator</a> to continue using this service.": "Моля, <a>свържете се с администратора на услугата</a> за да продължите да я използвате.",
     "Sorry, your homeserver is too old to participate in this room.": "Съжаляваме, вашият сървър е прекалено стар за да участва в тази стая.",
-    "Please contact your homeserver administrator.": "Моля, свържете се се със сървърния администратор.",
+    "Please contact your homeserver administrator.": "Моля, свържете се със сървърния администратор.",
     "Legal": "Юридически",
     "Unable to connect to Homeserver. Retrying...": "Неуспешно свързване със сървъра. Опитване отново...",
     "This room has been replaced and is no longer active.": "Тази стая е била заменена и вече не е активна.",
@@ -1612,11 +1612,11 @@
     "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s премахна правилото блокиращо достъпа до стаи отговарящи на %(glob)s",
     "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s премахна правилото блокиращо достъпа до сървъри отговарящи на %(glob)s",
     "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s премахна правилото блокиращо достъпа неща отговарящи на %(glob)s",
-    "%(senderName)s updated an invalid ban rule": "%(senderName)s обнови невалидно правило за блокиране",
-    "%(senderName)s updated the rule banning users 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 updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s премахна правилото блокиращо достъпа до сървъри отговарящи на %(glob)s поради %(reason)s",
-    "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s премахна правилото блокиращо достъпа неща отговарящи на %(glob)s поради %(reason)s",
+    "%(senderName)s updated an invalid ban rule": "%(senderName)s промени невалидно правило за блокиране",
+    "%(senderName)s updated the rule banning users 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 updated the rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s промени правилото блокиращо достъпа до сървъри отговарящи на %(glob)s поради %(reason)s",
+    "%(senderName)s updated a ban rule matching %(glob)s for %(reason)s": "%(senderName)s промени правило блокиращо достъпа неща отговарящи на %(glob)s поради %(reason)s",
     "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s създаде правило блокиращо достъпа на потребители отговарящи на %(glob)s поради %(reason)s",
     "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s създаде правило блокиращо достъпа до стаи отговарящи на %(glob)s поради %(reason)s",
     "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s създаде правило блокиращо достъпа до сървъри отговарящи на %(glob)s поради %(reason)s",
@@ -1949,7 +1949,7 @@
     "Accepting…": "Приемане…",
     "Start Verification": "Започни верификация",
     "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Съобщенията ви са защитени и само вие и получателят имате уникалните ключове за да ги отключите.",
-    "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "В шифровани стаи,  съобщенията ви са защитени и само вие и получателят имате уникалните ключове за да ги отключите.",
+    "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "В шифровани стаи, съобщенията ви са защитени и само вие и получателят имате уникалните ключове за да ги отключите.",
     "Verify User": "Потвърди потребителя",
     "For extra security, verify this user by checking a one-time code on both of your devices.": "За допълнителна сигурност, потвърдете този потребител като проверите еднократен код на устройствата ви.",
     "Your messages are not secure": "Съобщенията ви не са защитени",
@@ -2530,5 +2530,306 @@
     "%(brand)s Desktop": "%(brand)s Desktop",
     "%(brand)s Web": "Уеб версия на %(brand)s",
     "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>.": "Въведете адреса на вашия Element Matrix Services сървър. Той или използва ваш собствен домейн или е поддомейн на <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.": "Може да използвате опцията за собствен сървър, за да влезете в друг Matrix сървър, чрез указване на адреса му. Това позволява да използвате %(brand)s с Matrix профил съществуващ на друг сървър."
+    "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.": "Може да използвате опцията за собствен сървър, за да влезете в друг Matrix сървър, чрез указване на адреса му. Това позволява да използвате %(brand)s с Matrix профил съществуващ на друг сървър.",
+    "Forgot password?": "Забравена парола?",
+    "Search (must be enabled)": "Търсене (трябва да е включено)",
+    "Go to Home View": "Отиване на начален изглед",
+    "%(creator)s created this DM.": "%(creator)s създаде този директен чат.",
+    "You have no visible notifications.": "Нямате видими уведомления.",
+    "Filter rooms and people": "Филтриране на стаи и хора",
+    "Got an account? <a>Sign in</a>": "Имате профил? <a>Влезте от тук</a>",
+    "New here? <a>Create an account</a>": "Вие сте нов тук? <a>Създайте профил</a>",
+    "There was a problem communicating with the homeserver, please try again later.": "Възникна проблем при комуникацията със Home сървъра, моля опитайте отново по-късно.",
+    "New? <a>Create account</a>": "Вие сте нов? <a>Създайте профил</a>",
+    "That username already exists, please try another.": "Това потребителско име е вече заето, моля опитайте с друго.",
+    "Continue with %(ssoButtons)s": "Продължаване с %(ssoButtons)s",
+    "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Или %(usernamePassword)s",
+    "Already have an account? <a>Sign in here</a>": "Вече имате профил? <a>Влезте от тук</a>",
+    "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Ние ще запазим шифровано копие на вашите ключове на нашият сървър. Защитете вашето резервно копие с фраза за сигурност.",
+    "Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Ключа за сигурност беше <b>копиран в клиборда</b>, поставете го в:",
+    "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Тази сесия откри, че вашата фраза за сигурност и ключ за защитени съобщения бяха премахнати.",
+    "A new Security Phrase and key for Secure Messages have been detected.": "Новa фраза за сигурност и ключ за защитени съобщения бяха открити.",
+    "Use Security Key or Phrase": "Използване на ключ или фраза за сигурност",
+    "Use Security Key": "Използване на ключ за сигурност",
+    "Great! This Security Phrase looks strong enough.": "Чудесно! Тази фраза за сигурност изглежда достатъчно силна.",
+    "Set up with a Security Key": "Настройка със ключ за сигурност",
+    "Please enter your Security Phrase a second time to confirm.": "Моля въведете фразата си за сигурност повторно за да потвърдите.",
+    "Repeat your Security Phrase...": "Повторете фразата си за сигурност...",
+    "Your Security Key is in your <b>Downloads</b> folder.": "Ключа за сигурност е във вашата папка <b>Изтегляния</b>.",
+    "Your Security Key": "Вашият ключ за сигурност",
+    "Secure your backup with a Security Phrase": "Защитете вашето резервно копие с фраза за сигурност",
+    "Make a copy of your Security Key": "Направете копие на вашият ключ за сигурност",
+    "Confirm your Security Phrase": "Потвърдете вашата фраза за сигурност",
+    "Converts the DM to a room": "Превръща директния чат в стая",
+    "Converts the room to a DM": "Превръща стаята в директен чат",
+    "Takes the call in the current room off hold": "Възстановява повикването в текущата стая",
+    "Places the call in the current room on hold": "Задържа повикването в текущата стая",
+    "Permission is granted to use the webcam": "Разрешение за използване на уеб камерата е дадено",
+    "Call failed because webcam or microphone could not be accessed. Check that:": "Неуспешно повикване поради неуспешен достъп до уеб камера или микрофон. Проверете дали:",
+    "A microphone and webcam are plugged in and set up correctly": "Микрофон и уеб камера са включени и настроени правилно",
+    "We couldn't log you in": "Не можахме да ви впишем",
+    "You've reached the maximum number of simultaneous calls.": "Достигнахте максималният брой едновременни повиквания.",
+    "No other application is using the webcam": "Никое друго приложение не използва уеб камерата",
+    "Unable to access webcam / microphone": "Неуспешен достъп до уеб камера / микрофон",
+    "Unable to access microphone": "Неуспешен достъп до микрофон",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Неуспешно свързване към вашият Home сървър. Моля затворете този прозорец и опитайте отново.",
+    "Abort": "Прекрати",
+    "%(hostSignupBrand)s Setup": "Настройка на %(hostSignupBrand)s",
+    "Maximize dialog": "Максимизирай прозореца",
+    "Minimize dialog": "Минимизирай прозореца",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Научете повече в нашите <privacyPolicyLink />, <termsOfServiceLink /> и <cookiePolicyLink />.",
+    "Cookie Policy": "Политика за бисквитките",
+    "Privacy Policy": "Политика за поверителност",
+    "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Добавя ┬──┬ ノ( ゜-゜ノ) в началото на съобщението",
+    "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Добавя (╯°□°)╯︵ ┻━┻ в началото на съобщението",
+    "Effects": "Ефекти",
+    "Anguilla": "Ангила",
+    "British Indian Ocean Territory": "Британска територия в Индийския океан",
+    "Pitcairn Islands": "острови Питкерн",
+    "Heard & McDonald Islands": "острови Хърд и Макдоналд",
+    "Cook Islands": "острови Кук",
+    "Christmas Island": "остров Рождество",
+    "Brunei": "Бруней Даруссалам",
+    "Bouvet Island": "остров Буве",
+    "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": "Тонга",
+    "Timor-Leste": "Източен Тимор",
+    "Tokelau": "Токелау",
+    "Togo": "Того",
+    "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": "Полша",
+    "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": "Хондурас",
+    "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": "Фарьорски острови",
+    "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": "Коста Рика",
+    "Congo - Kinshasa": "Конго (Киншаса)",
+    "Congo - Brazzaville": "Конго (Бразавил)",
+    "Comoros": "Коморски острови",
+    "Colombia": "Колумбия",
+    "Cocos (Keeling) Islands": "Кокосови острови",
+    "China": "Китай",
+    "Chile": "Чили",
+    "Chad": "Чад",
+    "Central African Republic": "Централноафриканска република",
+    "Cayman Islands": "Кайманови острови",
+    "Caribbean Netherlands": "Карибска Нидерландия",
+    "Cape Verde": "Кабо Верде",
+    "Canada": "Канада",
+    "Cameroon": "Камерун",
+    "Cambodia": "Камбоджа",
+    "Burundi": "Бурунди",
+    "Burkina Faso": "Буркина Фасо",
+    "Bulgaria": "България",
+    "British Virgin Islands": "Британски Вирджински острови",
+    "Brazil": "Бразилия",
+    "Botswana": "Ботсвана",
+    "Bosnia": "Босна и Херцеговина",
+    "Bolivia": "Боливия",
+    "Bhutan": "Бутан",
+    "Bermuda": "Бермудски острови",
+    "Benin": "Бенин",
+    "Belize": "Белиз",
+    "Belgium": "Белгия",
+    "Belarus": "Беларус",
+    "Barbados": "Барбадос",
+    "Bangladesh": "Бангладеш",
+    "Bahrain": "Бахрейн",
+    "Bahamas": "Бахамски острови",
+    "Azerbaijan": "Азербайджан",
+    "Austria": "Австрия",
+    "Australia": "Австралия",
+    "Aruba": "Аруба",
+    "Armenia": "Армения",
+    "Argentina": "Аржентина",
+    "Antigua & Barbuda": "Антигуа и Барбуда",
+    "Antarctica": "Антарктика",
+    "Angola": "Ангола",
+    "Andorra": "Андора",
+    "American Samoa": "Американска Самоа",
+    "Algeria": "Алжир",
+    "Albania": "Албания",
+    "Åland Islands": "Оландски острови",
+    "Afghanistan": "Афганистан",
+    "United States": "Съединените щати",
+    "United Kingdom": "Обединеното кралство"
 }

From e6fbcbce6e653a777086cf685ac58b6ea0c1f141 Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Wed, 10 Feb 2021 16:33:51 +0000
Subject: [PATCH 146/389] Translated using Weblate (Albanian)

Currently translated at 99.6% (2755 of 2764 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 | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index cf9c316019..e8e47ac81b 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -3043,5 +3043,18 @@
     "Show line numbers in code blocks": "Shfaq numra rreshtat në blloqe kodi",
     "Expand code blocks by default": "Zgjeroji blloqet e kodit, si parazgjedhje",
     "Show stickers button": "Shfaq buton ngjitësish",
-    "Recently visited rooms": "Dhoma të vizituara së fundi"
+    "Recently visited rooms": "Dhoma të vizituara së fundi",
+    "Upgrade to pro": "Përmirësojeni me pro",
+    "Minimize dialog": "Minimizoje dialogun",
+    "Maximize dialog": "Zmadhoje plotësisht dialogun",
+    "%(hostSignupBrand)s Setup": "Ujdisje %(hostSignupBrand)s",
+    "You should know": "Duhet të dini",
+    "Privacy Policy": "Rregulla Privatësie",
+    "Cookie Policy": "Rregulla Cookie-sh",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Mësoni më tepër te <privacyPolicyLink />, <termsOfServiceLink /> dhe <cookiePolicyLink /> tonat.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Vazhdimi lejon përkohësisht procesin e ujdisjes së %(hostSignupBrand)s të hyjë në llogarinë tuaj dhe të sjellë adresa email të verifikuara. Këto të dhëna nuk depozitohen.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "S’u arrit të lidhej me shërbyesin tuaj Home. Ju lutemi, mbylleni këtë dialog dhe riprovoni.",
+    "Abort": "Ndërprite",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Jeni i sigurt se doni të ndërpritet krijimi i strehës? Procesi s’mund të vazhdohet.",
+    "Confirm abort of host creation": "Ripohoni ndërprerjen e krijimit të strehës"
 }

From a274ec95010971b5f81f25054f2921e6367c65b2 Mon Sep 17 00:00:00 2001
From: nilsjha <nils@gaupne.net>
Date: Thu, 18 Feb 2021 20:56:06 +0000
Subject: [PATCH 147/389] Translated using Weblate (Norwegian Nynorsk)

Currently translated at 44.8% (1240 of 2764 strings)

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

diff --git a/src/i18n/strings/nn.json b/src/i18n/strings/nn.json
index d7ad9e8320..478f05b5cb 100644
--- a/src/i18n/strings/nn.json
+++ b/src/i18n/strings/nn.json
@@ -1346,7 +1346,7 @@
     "This session is backing up your keys. ": "Denne økta har aktivert sikkerheitskopiering av nøklane dine ",
     "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Lag sikkerheitskopiar av krypteringsnøklane saman med kontoinnstillingane, slik at du kan gjenopprette data viss det skulle skje at du å mister tilgang til øktene dine. Sikkerheitskopiane er beskytta med ein unik gjenopprettingsnøkkel (Recovery Key).",
     "Encryption": "Kryptografi",
-    "Use Ctrl + Enter to send a message": "Bruk tastekombinasjonen Ctrl + Enter for å sende meldingar",
+    "Use Ctrl + Enter to send a message": "Bruk Ctrl + Enter for å sende meldingar",
     "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Skriv  namnet på skrifttypen(fonten) og %(brand)s forsøka å henta den frå operativsystemet.",
     "Use a system font": "Bruk tilpassa skrifttype henta frå operativsystemet",
     "System font name": "Namn på skrifttype",
@@ -1371,5 +1371,10 @@
     "List options": "Sjå alternativ",
     "Explore Public Rooms": "Utforsk offentlege rom",
     "Explore all public rooms": "Utforsk alle offentlege rom",
-    "Explore public rooms": "Utforsk offentlege rom"
+    "Explore public rooms": "Utforsk offentlege rom",
+    "Use Ctrl + F to search": "Bruk Ctrl + F for søk",
+    "Identity Server": "Identitetstenar",
+    "Email Address": "E-postadresse",
+    "Go Back": "Gå attende",
+    "Notification settings": "Varslingsinnstillingar"
 }

From 50c5a2d5fc43084b880e1d1a46189ac7db6d4b66 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Wed, 10 Feb 2021 18:12:10 +0000
Subject: [PATCH 148/389] Translated using Weblate (Estonian)

Currently translated at 100.0% (2764 of 2764 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 | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 4d129c2ca3..7769cf61c8 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -3053,5 +3053,18 @@
     "Share your screen": "Jaga oma ekraani",
     "Show line numbers in code blocks": "Näita koodiblokkides reanumbreid",
     "Expand code blocks by default": "Vaikimisi kuva koodiblokid tervikuna",
-    "Recently visited rooms": "Hiljuti külastatud jututoad"
+    "Recently visited rooms": "Hiljuti külastatud jututoad",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Jätkates ajutistel alustel, on sul võimalik %(hostSignupBrand)s seadistamisega edasi minna ning kontole ligipääsuka laadida verifitseeritud e-posti aadress. Seda teavet ei salvestata.",
+    "Abort": "Katkesta",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Kas sa oled kindel, et soovid katkestada seoste loomise? Sel juhul me edasi ei jätka.",
+    "Confirm abort of host creation": "Kinnita seoste loomise katkestamine",
+    "Upgrade to pro": "Võta kasutusele tasuline teenus",
+    "Minimize dialog": "Tee aken väikeseks",
+    "Maximize dialog": "Tee aken suureks",
+    "%(hostSignupBrand)s Setup": "%(hostSignupBrand)s seadistus",
+    "You should know": "Sa peaksid teadma",
+    "Privacy Policy": "Privaatsuspoliitika",
+    "Cookie Policy": "Küpsiste kasutamine",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Lisateavet leiad <privacyPolicyLink />, <termsOfServiceLink /> ja <cookiePolicyLink /> lehtedelt.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Ei õnnestunud ühendada koduserveriga. Palun sulge see aken ja proovi uuesti."
 }

From e3f08adacb082ef0f6e72e7b20adc494a86aae5f Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Wed, 24 Feb 2021 17:27:56 +0000
Subject: [PATCH 149/389] Upgrade matrix-js-sdk to 9.8.0-rc.1

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

diff --git a/package.json b/package.json
index d4f931d811..7b4e577406 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
     "katex": "^0.12.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.20",
-    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
+    "matrix-js-sdk": "9.8.0-rc.1",
     "matrix-widget-api": "^0.1.0-beta.13",
     "minimist": "^1.2.5",
     "pako": "^2.0.3",
diff --git a/yarn.lock b/yarn.lock
index 01450908cc..bef26e27b4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5572,9 +5572,10 @@ mathml-tag-names@^2.1.3:
   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.7.0"
-  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c82bc35202f93efa2cb9b27b140f83df37c64ab2"
+matrix-js-sdk@9.8.0-rc.1:
+  version "9.8.0-rc.1"
+  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.8.0-rc.1.tgz#229122583bec5971f22a423a4a40d749e07602d9"
+  integrity sha512-Tmo5cdyBBgYcMZMaAavEvtdCsEwr5sYE0RLd6etLOSTxmGRSYpqKvvKQqGsYrogmZYNbx9nNZYYYV2aJkCKcQg==
   dependencies:
     "@babel/runtime" "^7.12.5"
     another-json "^0.2.0"

From c78d1c49abc6b278f16e289681ed61a7d3f5dd0e Mon Sep 17 00:00:00 2001
From: Will Hunt <willh@matrix.org>
Date: Wed, 24 Feb 2021 17:32:00 +0000
Subject: [PATCH 150/389] Apply changes from review

---
 src/components/structures/LoggedInView.tsx | 4 ++--
 src/toasts/ServerLimitToast.tsx            | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index f3cce3ef34..c01214f3f4 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -305,7 +305,7 @@ class LoggedInView extends React.Component<IProps, IState> {
         }
     };
 
-    _onHideToast = () => {
+    private onUsageLimitDismissed = () => {
         this.setState({
             usageLimitDismissed: true,
         });
@@ -322,7 +322,7 @@ class LoggedInView extends React.Component<IProps, IState> {
         if (usageLimitEventContent && this.state.usageLimitDismissed) {
             showServerLimitToast(
                 usageLimitEventContent.limit_type,
-                this._onHideToast,
+                this.onUsageLimitDismissed,
                 usageLimitEventContent.admin_contact,
                 error,
             );
diff --git a/src/toasts/ServerLimitToast.tsx b/src/toasts/ServerLimitToast.tsx
index 068d62f9ea..28c0ec9598 100644
--- a/src/toasts/ServerLimitToast.tsx
+++ b/src/toasts/ServerLimitToast.tsx
@@ -40,7 +40,7 @@ export const showToast = (limitType: string, onHideToast: () => void, adminConta
             acceptLabel: _t("Ok"),
             onAccept: () => {
                 hideToast()
-                onHideToast()
+                if (onHideToast) { onHideToast() }
             },
         },
         component: GenericToast,

From 364f24851355fe0b1a716cd5973bbc19cf575d7a Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Wed, 24 Feb 2021 17:32:53 +0000
Subject: [PATCH 151/389] Prepare changelog for v3.15.0-rc.1

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c87f1c62e6..e727adabfa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,65 @@
+Changes in [3.15.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0-rc.1) (2021-02-24)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0...v3.15.0-rc.1)
+
+ * Upgrade to JS SDK 9.8.0-rc.1
+ * Translations update from Weblate
+   [\#5683](https://github.com/matrix-org/matrix-react-sdk/pull/5683)
+ * Fix object diffing when objects have different keys
+   [\#5681](https://github.com/matrix-org/matrix-react-sdk/pull/5681)
+ * Add <code> if it's missing
+   [\#5673](https://github.com/matrix-org/matrix-react-sdk/pull/5673)
+ * Add email only if the verification is complete
+   [\#5629](https://github.com/matrix-org/matrix-react-sdk/pull/5629)
+ * Fix portrait videocalls
+   [\#5676](https://github.com/matrix-org/matrix-react-sdk/pull/5676)
+ * Tweak code block icon positions
+   [\#5643](https://github.com/matrix-org/matrix-react-sdk/pull/5643)
+ * Revert "Improve URL preview formatting and image upload thumbnail size"
+   [\#5677](https://github.com/matrix-org/matrix-react-sdk/pull/5677)
+ * Fix context menu leaving visible area
+   [\#5644](https://github.com/matrix-org/matrix-react-sdk/pull/5644)
+ * Jitsi conferences names, take 3
+   [\#5675](https://github.com/matrix-org/matrix-react-sdk/pull/5675)
+ * Update isUserOnDarkTheme to take use_system_theme in account
+   [\#5670](https://github.com/matrix-org/matrix-react-sdk/pull/5670)
+ * Discard some dead code
+   [\#5665](https://github.com/matrix-org/matrix-react-sdk/pull/5665)
+ * Add developer tool to explore and edit settings
+   [\#5664](https://github.com/matrix-org/matrix-react-sdk/pull/5664)
+ * Use and create new room helpers
+   [\#5663](https://github.com/matrix-org/matrix-react-sdk/pull/5663)
+ * Clear message previews when the maximum limit is reached for history
+   [\#5661](https://github.com/matrix-org/matrix-react-sdk/pull/5661)
+ * VoIP virtual rooms, mk II
+   [\#5639](https://github.com/matrix-org/matrix-react-sdk/pull/5639)
+ * Disable chat effects when reduced motion preferred
+   [\#5660](https://github.com/matrix-org/matrix-react-sdk/pull/5660)
+ * Improve URL preview formatting and image upload thumbnail size
+   [\#5637](https://github.com/matrix-org/matrix-react-sdk/pull/5637)
+ * Fix border radius when the panel is collapsed
+   [\#5641](https://github.com/matrix-org/matrix-react-sdk/pull/5641)
+ * Use a more generic layout setting - useIRCLayout → layout
+   [\#5571](https://github.com/matrix-org/matrix-react-sdk/pull/5571)
+ * Remove redundant lockOrigin parameter from usercontent
+   [\#5657](https://github.com/matrix-org/matrix-react-sdk/pull/5657)
+ * Set ICE candidate pool size option
+   [\#5655](https://github.com/matrix-org/matrix-react-sdk/pull/5655)
+ * Prepare to encrypt when a call arrives
+   [\#5654](https://github.com/matrix-org/matrix-react-sdk/pull/5654)
+ * Use config for host signup branding
+   [\#5650](https://github.com/matrix-org/matrix-react-sdk/pull/5650)
+ * Use randomly generated conference names for Jitsi
+   [\#5649](https://github.com/matrix-org/matrix-react-sdk/pull/5649)
+ * Modified regex to account for an immediate new line after slash commands
+   [\#5647](https://github.com/matrix-org/matrix-react-sdk/pull/5647)
+ * Fix codeblock scrollbar color for non-Firefox
+   [\#5642](https://github.com/matrix-org/matrix-react-sdk/pull/5642)
+ * Fix codeblock scrollbar colors
+   [\#5630](https://github.com/matrix-org/matrix-react-sdk/pull/5630)
+ * Added loading and disabled the button while searching for server
+   [\#5634](https://github.com/matrix-org/matrix-react-sdk/pull/5634)
+
 Changes in [3.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0) (2021-02-16)
 =====================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0-rc.1...v3.14.0)

From 8860c8bfe7b48cbfb230f28e81d48ea7f474e3f4 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Wed, 24 Feb 2021 17:32:54 +0000
Subject: [PATCH 152/389] v3.15.0-rc.1

---
 package.json | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json
index 7b4e577406..f49b99831f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "3.14.0",
+  "version": "3.15.0-rc.1",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {
@@ -27,7 +27,7 @@
     "matrix-gen-i18n": "scripts/gen-i18n.js",
     "matrix-prune-i18n": "scripts/prune-i18n.js"
   },
-  "main": "./src/index.js",
+  "main": "./lib/index.js",
   "matrix_src_main": "./src/index.js",
   "matrix_lib_main": "./lib/index.js",
   "matrix_lib_typings": "./lib/index.d.ts",
@@ -189,5 +189,6 @@
     "transformIgnorePatterns": [
       "/node_modules/(?!matrix-js-sdk).+$"
     ]
-  }
+  },
+  "typings": "./lib/index.d.ts"
 }

From c11c8c2fafc23564e9cacbfc947e941e341b3cc3 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 24 Feb 2021 11:20:58 -0700
Subject: [PATCH 153/389] Update src/toasts/ServerLimitToast.tsx

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

diff --git a/src/toasts/ServerLimitToast.tsx b/src/toasts/ServerLimitToast.tsx
index 28c0ec9598..81b5492402 100644
--- a/src/toasts/ServerLimitToast.tsx
+++ b/src/toasts/ServerLimitToast.tsx
@@ -40,7 +40,7 @@ export const showToast = (limitType: string, onHideToast: () => void, adminConta
             acceptLabel: _t("Ok"),
             onAccept: () => {
                 hideToast()
-                if (onHideToast) { onHideToast() }
+                if (onHideToast) onHideToast();
             },
         },
         component: GenericToast,

From 5f74fac2e84057ff9155474416e00793c671c89f Mon Sep 17 00:00:00 2001
From: Hubert Chathi <hubert@uhoreg.ca>
Date: Wed, 24 Feb 2021 17:55:27 -0500
Subject: [PATCH 154/389] fall back to the old method if the default key isn't
 available

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

diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts
index 11d228e7ab..03cbe88c22 100644
--- a/src/SecurityManager.ts
+++ b/src/SecurityManager.ts
@@ -105,9 +105,12 @@ async function getSecretStorageKey(
         // use the default SSSS key if set
         keyInfo = keyInfos[keyId];
         if (!keyInfo) {
-            throw new Error("Unable to use default SSSS key");
+            // if the default key is not available, pretend the default key
+            // isn't set
+            keyId = undefined;
         }
-    } else {
+    }
+    if (!keyId) {
         // if no default SSSS key is set, fall back to a heuristic of using the
         // only available key, if only one key is set
         const keyInfoEntries = Object.entries(keyInfos);

From e2fb9b3ae878fcf68f1b4dd72b3badf16e0b2e13 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 24 Feb 2021 18:10:35 -0700
Subject: [PATCH 155/389] Clean up widgets when leaving the room

---
 .../views/elements/PersistentApp.js           | 26 +++++++++++++++----
 1 file changed, 21 insertions(+), 5 deletions(-)

diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js
index a1e805c085..eb5f4eab7d 100644
--- a/src/components/views/elements/PersistentApp.js
+++ b/src/components/views/elements/PersistentApp.js
@@ -31,6 +31,7 @@ export default class PersistentApp extends React.Component {
     componentDidMount() {
         this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
         ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
+        MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
     }
 
     componentWillUnmount() {
@@ -38,6 +39,9 @@ export default class PersistentApp extends React.Component {
             this._roomStoreToken.remove();
         }
         ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
+        if (MatrixClientPeg.get()) {
+            MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership);
+        }
     }
 
     _onRoomViewStoreUpdate = payload => {
@@ -53,16 +57,28 @@ export default class PersistentApp extends React.Component {
         });
     };
 
+    _onMyMembership = async (room) => {
+        const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
+        if (room.getMyMembership() !== "join") {
+            // we're not in the room anymore - delete
+            if (room.roomId === persistentWidgetInRoomId) {
+                ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
+            }
+        }
+    };
+
     render() {
         if (this.state.persistentWidgetId) {
             const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
-            if (this.state.roomId !== persistentWidgetInRoomId) {
-                const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
 
-                // Sanity check the room - the widget may have been destroyed between render cycles, and
-                // thus no room is associated anymore.
-                if (!persistentWidgetInRoom) return null;
+            const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
 
+            // Sanity check the room - the widget may have been destroyed between render cycles, and
+            // thus no room is associated anymore.
+            if (!persistentWidgetInRoom) return null;
+
+            const myMembership = persistentWidgetInRoom.getMyMembership();
+            if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
                 // get the widget data
                 const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
                     return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();

From fbe5d177851ce836ca33afb09ab981511e4c87b3 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 24 Feb 2021 18:27:59 -0700
Subject: [PATCH 156/389] sanity

---
 src/components/views/elements/PersistentApp.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js
index eb5f4eab7d..7801076c66 100644
--- a/src/components/views/elements/PersistentApp.js
+++ b/src/components/views/elements/PersistentApp.js
@@ -57,9 +57,9 @@ export default class PersistentApp extends React.Component {
         });
     };
 
-    _onMyMembership = async (room) => {
+    _onMyMembership = async (room, membership) => {
         const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
-        if (room.getMyMembership() !== "join") {
+        if (membership !== "join") {
             // we're not in the room anymore - delete
             if (room.roomId === persistentWidgetInRoomId) {
                 ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);

From 864a9974b1fe6015d56c6bf1987e19a354b3378d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 25 Feb 2021 12:20:10 +0000
Subject: [PATCH 157/389] Tweak spaces labs flag copy

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

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 77451c1da8..973c426420 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -777,7 +777,7 @@
     "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
     "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
     "Change notification settings": "Change notification settings",
-    "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags",
+    "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
     "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",
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index a8fa88179b..b452f10e73 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -122,7 +122,8 @@ export interface ISetting {
 export const SETTINGS: {[setting: string]: ISetting} = {
     "feature_spaces": {
         isFeature: true,
-        displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags"),
+        displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " +
+            "Requires compatible homeserver for some features."),
         supportedLevels: LEVELS_FEATURE,
         default: false,
         controller: new ReloadOnChangeController(),

From 7030c636f07c32c3616eeddba64e0a0c8dfc6587 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 25 Feb 2021 15:18:54 +0000
Subject: [PATCH 158/389] Initial Space Store for keeping track of space
 hierarchies from sync

---
 src/@types/global.d.ts                        |   2 +
 src/stores/SpaceStore.tsx                     | 462 ++++++++++++++++++
 .../notifications/SpaceNotificationState.ts   |  82 ++++
 3 files changed, 546 insertions(+)
 create mode 100644 src/stores/SpaceStore.tsx
 create mode 100644 src/stores/notifications/SpaceNotificationState.ts

diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 28f22780a2..4aa6df5488 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -38,6 +38,7 @@ import UserActivity from "../UserActivity";
 import {ModalWidgetStore} from "../stores/ModalWidgetStore";
 import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
 import VoipUserMapper from "../VoipUserMapper";
+import {SpaceStoreClass} from "../stores/SpaceStore";
 
 declare global {
     interface Window {
@@ -68,6 +69,7 @@ declare global {
         mxUserActivity: UserActivity;
         mxModalWidgetStore: ModalWidgetStore;
         mxVoipUserMapper: VoipUserMapper;
+        mxSpaceStore: SpaceStoreClass;
     }
 
     interface Document {
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
new file mode 100644
index 0000000000..d675879138
--- /dev/null
+++ b/src/stores/SpaceStore.tsx
@@ -0,0 +1,462 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {throttle, sortBy} from "lodash";
+import {EventType} from "matrix-js-sdk/src/@types/event";
+import {Room} from "matrix-js-sdk/src/models/room";
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+
+import {AsyncStoreWithClient} from "./AsyncStoreWithClient";
+import defaultDispatcher from "../dispatcher/dispatcher";
+import {ActionPayload} from "../dispatcher/payloads";
+import RoomListStore from "./room-list/RoomListStore";
+import SettingsStore from "../settings/SettingsStore";
+import DMRoomMap from "../utils/DMRoomMap";
+import {FetchRoomFn} from "./notifications/ListNotificationState";
+import {SpaceNotificationState} from "./notifications/SpaceNotificationState";
+import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
+import {DefaultTagID} from "./room-list/models";
+import {EnhancedMap, mapDiff} from "../utils/maps";
+import {setHasDiff} from "../utils/sets";
+import {objectDiff} from "../utils/objects";
+import {arrayHasDiff} from "../utils/arrays";
+
+type SpaceKey = string | symbol;
+
+interface IState {}
+
+const ACTIVE_SPACE_LS_KEY = "mx_active_space";
+
+export const HOME_SPACE = Symbol("home-space");
+
+export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
+export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
+// Space Room ID/HOME_SPACE will be emitted when a Space's children change
+
+const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
+    return arr.reduce((result, room: Room) => {
+        result[room.isSpaceRoom() ? 0 : 1].push(room);
+        return result;
+    }, [[], []]);
+};
+
+const getOrder = (ev: MatrixEvent): string | null => {
+    const content = ev.getContent();
+    if (typeof content.order === "string" && Array.from(content.order).every((c: string) => {
+        const charCode = c.charCodeAt(0);
+        return charCode >= 0x20 && charCode <= 0x7F;
+    })) {
+        return content.order;
+    }
+    return null;
+}
+
+const getRoomFn: FetchRoomFn = (room: Room) => {
+    return RoomNotificationStateStore.instance.getRoomState(room);
+};
+
+export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
+    constructor() {
+        super(defaultDispatcher, {});
+    }
+
+    // The spaces representing the roots of the various tree-like hierarchies
+    private rootSpaces: Room[] = [];
+    // The list of rooms not present in any currently joined spaces
+    private orphanedRooms = new Set<string>();
+    // Map from room ID to set of spaces which list it as a child
+    private parentMap = new EnhancedMap<string, Set<string>>();
+    // Map from space key to SpaceNotificationState instance representing that space
+    private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
+    // Map from space key to Set of room IDs that should be shown as part of that space's filter
+    private spaceFilteredRooms = new Map<string | symbol, Set<string>>();
+    // The space currently selected in the Space Panel - if null then `Home` is selected
+    private _activeSpace?: Room = null;
+
+    public get spacePanelSpaces(): Room[] {
+        return this.rootSpaces;
+    }
+
+    public get activeSpace(): Room | null {
+        return this._activeSpace || null;
+    }
+
+    public setActiveSpace(space: Room | null) {
+        if (space === this.activeSpace) return;
+
+        this._activeSpace = space;
+        this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
+
+        // persist space selected
+        if (space) {
+            window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space.roomId);
+        } else {
+            window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
+        }
+    }
+
+    public addRoomToSpace(space: Room, roomId: string, via: string[], autoJoin = false) {
+        return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, {
+            via,
+            auto_join: autoJoin,
+        }, roomId);
+    }
+
+    private getChildren(spaceId: string): Room[] {
+        const room = this.matrixClient?.getRoom(spaceId);
+        const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via);
+        return sortBy(childEvents, getOrder)
+            .map(ev => this.matrixClient.getRoom(ev.getStateKey()))
+            .filter(room => room?.getMyMembership() === "join") || [];
+    }
+
+    public getChildRooms(spaceId: string): Room[] {
+        return this.getChildren(spaceId).filter(r => !r.isSpaceRoom());
+    }
+
+    public getChildSpaces(spaceId: string): Room[] {
+        return this.getChildren(spaceId).filter(r => r.isSpaceRoom());
+    }
+
+    public getParents(roomId: string, canonicalOnly = false): Room[] {
+        const room = this.matrixClient?.getRoom(roomId);
+        return room?.currentState.getStateEvents(EventType.SpaceParent)
+            .filter(ev => {
+                const content = ev.getContent();
+                if (!content?.via) return false;
+                // TODO apply permissions check to verify that the parent mapping is valid
+                if (canonicalOnly && !content?.canonical) return false;
+                return true;
+            })
+            .map(ev => this.matrixClient.getRoom(ev.getStateKey()))
+            .filter(Boolean) || [];
+    }
+
+    public getCanonicalParent(roomId: string): Room | null {
+        const parents = this.getParents(roomId, true);
+        return sortBy(parents, r => r.roomId)?.[0] || null;
+    }
+
+    public getSpaces = () => {
+        return this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "join");
+    };
+
+    public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
+        return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
+    };
+
+    public rebuild = throttle(() => { // exported for tests
+        const visibleRooms = this.matrixClient.getVisibleRooms();
+
+        // Sort spaces by room ID to force the loop breaking to be deterministic
+        const spaces = sortBy(this.getSpaces(), space => space.roomId);
+        const unseenChildren = new Set<Room>([...visibleRooms, ...spaces]);
+
+        const backrefs = new EnhancedMap<string, Set<string>>();
+
+        // TODO handle cleaning up links when a Space is removed
+        spaces.forEach(space => {
+            const children = this.getChildren(space.roomId);
+            children.forEach(child => {
+                unseenChildren.delete(child);
+
+                backrefs.getOrCreate(child.roomId, new Set()).add(space.roomId);
+            });
+        });
+
+        const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
+
+        // untested algorithm to handle full-cycles
+        const detachedNodes = new Set<Room>(spaces);
+
+        const markTreeChildren = (rootSpace: Room, unseen: Set<Room>) => {
+            const stack = [rootSpace];
+            while (stack.length) {
+                const op = stack.pop();
+                unseen.delete(op);
+                this.getChildSpaces(op.roomId).forEach(space => {
+                    if (unseen.has(space)) {
+                        stack.push(space);
+                    }
+                });
+            }
+        };
+
+        rootSpaces.forEach(rootSpace => {
+            markTreeChildren(rootSpace, detachedNodes);
+        });
+
+        // Handle spaces forming fully cyclical relationships.
+        // In order, assume each detachedNode is a root unless it has already
+        // been claimed as the child of prior detached node.
+        // Work from a copy of the detachedNodes set as it will be mutated as part of this operation.
+        Array.from(detachedNodes).forEach(detachedNode => {
+            if (!detachedNodes.has(detachedNode)) return;
+            // declare this detached node a new root, find its children, without ever looping back to it
+            detachedNodes.delete(detachedNode);
+            rootSpaces.push(detachedNode);
+            markTreeChildren(detachedNode, detachedNodes);
+
+            // TODO only consider a detached node a root space if it has no *parents other than the ones forming cycles
+        });
+
+        // TODO neither of these handle an A->B->C->A with an additional C->D
+        // detachedNodes.forEach(space => {
+        //     rootSpaces.push(space);
+        // });
+
+        this.orphanedRooms = new Set(orphanedRooms);
+        this.rootSpaces = rootSpaces;
+        this.parentMap = backrefs;
+
+        // if the currently selected space no longer exists, remove its selection
+        if (this._activeSpace && detachedNodes.has(this._activeSpace)) {
+            this.setActiveSpace(null);
+        }
+
+        this.onRoomsUpdate(); // TODO only do this if a change has happened
+        this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
+    }, 100, {trailing: true, leading: true});
+
+    onSpaceUpdate = () => {
+        this.rebuild();
+    }
+
+    private showInHomeSpace = (room: Room) => {
+        return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
+            || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
+            || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
+    };
+
+    // Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
+    // This can only change whether it shows up in the HOME_SPACE or not
+    private onRoomUpdate = (room: Room) => {
+        if (this.showInHomeSpace(room)) {
+            this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId);
+            this.emit(HOME_SPACE);
+        } else if (!this.orphanedRooms.has(room.roomId)) {
+            this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId);
+            this.emit(HOME_SPACE);
+        }
+    };
+
+    private onRoomsUpdate = throttle(() => {
+        // TODO resolve some updates as deltas
+        const visibleRooms = this.matrixClient.getVisibleRooms();
+
+        const oldFilteredRooms = this.spaceFilteredRooms;
+        this.spaceFilteredRooms = new Map();
+
+        // put all invites (rooms & spaces) in the Home Space
+        const invites = this.matrixClient.getRooms().filter(r => r.getMyMembership() === "invite");
+        this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
+
+        visibleRooms.forEach(room => {
+            if (this.showInHomeSpace(room)) {
+                this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
+            }
+        });
+
+        this.rootSpaces.forEach(s => {
+            // traverse each space tree in DFS to build up the supersets as you go up,
+            // reusing results from like subtrees.
+            const fn = (spaceId: string, parentPath: Set<string>): Set<string> => {
+                if (parentPath.has(spaceId)) return; // prevent cycles
+
+                // reuse existing results if multiple similar branches exist
+                if (this.spaceFilteredRooms.has(spaceId)) {
+                    return this.spaceFilteredRooms.get(spaceId);
+                }
+
+                const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId));
+                const roomIds = new Set(childRooms.map(r => r.roomId));
+                const space = this.matrixClient?.getRoom(spaceId);
+
+                // Add relevant DMs
+                space?.getJoinedMembers().forEach(member => {
+                    DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
+                        roomIds.add(roomId);
+                    });
+                });
+
+                const newPath = new Set(parentPath).add(spaceId);
+                childSpaces.forEach(childSpace => {
+                    fn(childSpace.roomId, newPath)?.forEach(roomId => {
+                        roomIds.add(roomId);
+                    });
+                });
+                this.spaceFilteredRooms.set(spaceId, roomIds);
+                return roomIds;
+            };
+
+            fn(s.roomId, new Set());
+        });
+
+        const diff = mapDiff(oldFilteredRooms, this.spaceFilteredRooms);
+        // filter out keys which changed by reference only by checking whether the sets differ
+        const changed = diff.changed.filter(k => setHasDiff(oldFilteredRooms.get(k), this.spaceFilteredRooms.get(k)));
+        [...diff.added, ...diff.removed, ...changed].forEach(k => {
+            this.emit(k);
+        });
+
+        this.spaceFilteredRooms.forEach((roomIds, s) => {
+            // Update NotificationStates
+            const rooms = this.matrixClient.getRooms().filter(room => roomIds.has(room.roomId));
+            this.getNotificationState(s)?.setRooms(rooms);
+        });
+    }, 100, {trailing: true, leading: true});
+
+    private onRoom = (room: Room) => {
+        if (room?.isSpaceRoom()) {
+            this.onSpaceUpdate();
+            this.emit(room.roomId);
+        } else {
+            // this.onRoomUpdate(room);
+            this.onRoomsUpdate();
+        }
+    };
+
+    private onRoomState = (ev: MatrixEvent) => {
+        const room = this.matrixClient.getRoom(ev.getRoomId());
+        if (!room) return;
+
+        if (ev.getType() === EventType.SpaceChild && room.isSpaceRoom()) {
+            this.onSpaceUpdate();
+            this.emit(room.roomId);
+        } else if (ev.getType() === EventType.SpaceParent) {
+            // TODO rebuild the space parent and not the room - check permissions?
+            // TODO confirm this after implementing parenting behaviour
+            if (room.isSpaceRoom()) {
+                this.onSpaceUpdate();
+            } else {
+                this.onRoomUpdate(room);
+            }
+            this.emit(room.roomId);
+        }
+    };
+
+    private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent: MatrixEvent) => {
+        if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) {
+            // If the room was in favourites and now isn't or the opposite then update its position in the trees
+            if (!!ev.getContent()[DefaultTagID.Favourite] !== !!lastEvent.getContent()[DefaultTagID.Favourite]) {
+                this.onRoomUpdate(room);
+            }
+        }
+    }
+
+    private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
+        if (ev.getType() === EventType.Direct) {
+            const lastContent = lastEvent.getContent();
+            const content = ev.getContent();
+
+            const diff = objectDiff<Record<string, string[]>>(lastContent, content);
+            // filter out keys which changed by reference only by checking whether the sets differ
+            const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k]));
+            // DM tag changes, refresh relevant rooms
+            new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => {
+                const room = this.matrixClient?.getRoom(roomId);
+                if (room) {
+                    this.onRoomUpdate(room);
+                }
+            });
+        }
+    };
+
+    protected async onNotReady() {
+        if (!SettingsStore.getValue("feature_spaces")) return;
+        if (this.matrixClient) {
+            this.matrixClient.removeListener("Room", this.onRoom);
+            this.matrixClient.removeListener("Room.myMembership", this.onRoom);
+            this.matrixClient.removeListener("RoomState.events", this.onRoomState);
+            this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
+            this.matrixClient.removeListener("accountData", this.onAccountData);
+        }
+        await this.reset({});
+    }
+
+    protected async onReady() {
+        if (!SettingsStore.getValue("feature_spaces")) return;
+        this.matrixClient.on("Room", this.onRoom);
+        this.matrixClient.on("Room.myMembership", this.onRoom);
+        this.matrixClient.on("RoomState.events", this.onRoomState);
+        this.matrixClient.on("Room.accountData", this.onRoomAccountData);
+        this.matrixClient.on("accountData", this.onAccountData);
+
+        await this.onSpaceUpdate(); // trigger an initial update
+
+        // restore selected state from last session if any and still valid
+        const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY);
+        if (lastSpaceId) {
+            const space = this.rootSpaces.find(s => s.roomId === lastSpaceId);
+            if (space) {
+                this.setActiveSpace(space);
+            }
+        }
+    }
+
+    protected async onAction(payload: ActionPayload) {
+        switch (payload.action) {
+            case "view_room": {
+                const room = this.matrixClient?.getRoom(payload.room_id);
+
+                if (room?.getMyMembership() === "join") {
+                    if (room.isSpaceRoom()) {
+                        this.setActiveSpace(room);
+                    } else if (!this.spaceFilteredRooms.get(this._activeSpace?.roomId || HOME_SPACE).has(room.roomId)) {
+                        // TODO maybe reverse these first 2 clauses once space panel active is fixed
+                        let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId));
+                        if (!parent) {
+                            parent = this.getCanonicalParent(room.roomId);
+                        }
+                        if (!parent) {
+                            const parents = Array.from(this.parentMap.get(room.roomId) || []);
+                            parent = parents.find(p => this.matrixClient.getRoom(p));
+                        }
+                        if (parent) {
+                            this.setActiveSpace(parent);
+                        }
+                    }
+                }
+                break;
+            }
+            case "after_leave_room":
+                if (this._activeSpace && payload.room_id === this._activeSpace.roomId) {
+                    this.setActiveSpace(null);
+                }
+                break;
+        }
+    }
+
+    public getNotificationState(key: SpaceKey): SpaceNotificationState {
+        if (this.notificationStateMap.has(key)) {
+            return this.notificationStateMap.get(key);
+        }
+
+        const state = new SpaceNotificationState(key, getRoomFn);
+        this.notificationStateMap.set(key, state);
+        return state;
+    }
+}
+
+export default class SpaceStore {
+    private static internalInstance = new SpaceStoreClass();
+
+    public static get instance(): SpaceStoreClass {
+        return SpaceStore.internalInstance;
+    }
+}
+
+window.mxSpaceStore = SpaceStore.instance;
diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts
new file mode 100644
index 0000000000..61a9701a07
--- /dev/null
+++ b/src/stores/notifications/SpaceNotificationState.ts
@@ -0,0 +1,82 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Room } from "matrix-js-sdk/src/models/room";
+
+import { NotificationColor } from "./NotificationColor";
+import { arrayDiff } from "../../utils/arrays";
+import { RoomNotificationState } from "./RoomNotificationState";
+import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState";
+import { FetchRoomFn } from "./ListNotificationState";
+
+export class SpaceNotificationState extends NotificationState {
+    private rooms: Room[] = [];
+    private states: { [spaceId: string]: RoomNotificationState } = {};
+
+    constructor(private spaceId: string | symbol, private getRoomFn: FetchRoomFn) {
+        super();
+    }
+
+    public get symbol(): string {
+        return null; // This notification state doesn't support symbols
+    }
+
+    public setRooms(rooms: Room[]) {
+        const oldRooms = this.rooms;
+        const diff = arrayDiff(oldRooms, rooms);
+        this.rooms = rooms;
+        for (const oldRoom of diff.removed) {
+            const state = this.states[oldRoom.roomId];
+            if (!state) continue; // We likely just didn't have a badge (race condition)
+            delete this.states[oldRoom.roomId];
+            state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
+        }
+        for (const newRoom of diff.added) {
+            const state = this.getRoomFn(newRoom);
+            state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
+            this.states[newRoom.roomId] = state;
+        }
+
+        this.calculateTotalState();
+    }
+
+    public destroy() {
+        super.destroy();
+        for (const state of Object.values(this.states)) {
+            state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
+        }
+        this.states = {};
+    }
+
+    private onRoomNotificationStateUpdate = () => {
+        this.calculateTotalState();
+    };
+
+    private calculateTotalState() {
+        const snapshot = this.snapshot();
+
+        this._count = 0;
+        this._color = NotificationColor.None;
+        for (const state of Object.values(this.states)) {
+            this._count += state.count;
+            this._color = Math.max(this.color, state.color);
+        }
+
+        // finally, publish an update if needed
+        this.emitIfUpdated(snapshot);
+    }
+}
+

From ad85764a8e3d1025c76db29e9f53ba690ac2eb4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 18:23:32 +0100
Subject: [PATCH 159/389] Fix timeline expansion
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_MainSplit.scss | 1 +
 res/css/structures/_RoomView.scss  | 1 +
 2 files changed, 2 insertions(+)

diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss
index f05f24d0d7..9597083e9c 100644
--- a/res/css/structures/_MainSplit.scss
+++ b/res/css/structures/_MainSplit.scss
@@ -19,6 +19,7 @@ limitations under the License.
     flex-direction: row;
     min-width: 0;
     height: 100%;
+    justify-content: space-between;
 }
 
 .mx_MainSplit > .mx_RightPanel_ResizeWrapper {
diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index cd8c640132..5240a0650f 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -121,6 +121,7 @@ limitations under the License.
     position: relative; //for .mx_RoomView_auxPanel_fullHeight
     display: flex;
     flex-direction: column;
+    width: 100%;
 }
 
 .mx_RoomView_body {

From aa4ec9fca1a65202306ee77d705b15f1782de188 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 18:27:52 +0100
Subject: [PATCH 160/389] Make $droptarget-bg-color more opaque
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/themes/light/css/_light.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 1c89d83c01..ea7b0472e0 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -68,7 +68,7 @@ $groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77);
 $plinth-bg-color: $secondary-accent-color;
 
 // used by RoomDropTarget
-$droptarget-bg-color: rgba(255,255,255,0.5);
+$droptarget-bg-color: rgba(255,255,255,0.95);
 
 // used by AddressSelector
 $selected-color: $secondary-accent-color;

From 8551855a5626d06e6feda6f849f50b26af85a194 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 18:30:14 +0100
Subject: [PATCH 161/389] Add $droptarget-bg-color to the dark theme
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/themes/dark/css/_dark.scss | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index a878aa3cdd..f6f415ce70 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -42,6 +42,9 @@ $preview-bar-bg-color: $header-panel-bg-color;
 $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
 $inverted-bg-color: $base-color;
 
+// used by RoomDropTarget
+$droptarget-bg-color: rgba(21,25,30,0.95);
+
 // used by AddressSelector
 $selected-color: $room-highlight-color;
 

From a3001f77e46e7c3ac3479a70eb56db312c6f1361 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 18:30:39 +0100
Subject: [PATCH 162/389] Remove rounded corners of the drop area
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 5240a0650f..e80ac062b6 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -30,9 +30,6 @@ limitations under the License.
 
     pointer-events: none;
 
-    border-top-left-radius: 10px;
-    border-top-right-radius: 10px;
-
     background-color: $droptarget-bg-color;
 
     position: absolute;

From 26b70b62280b7f4fa21c6287faf20a309a399abe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 18:32:04 +0100
Subject: [PATCH 163/389] Remove label background
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index e80ac062b6..8ba31fac20 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -42,11 +42,6 @@ limitations under the License.
 
 .mx_RoomView_fileDropTargetLabel {
     position: absolute;
-
-    border-radius: 10px;
-    padding: 10px;
-
-    background-color: $menu-bg-color;
 }
 
 .mx_RoomView_auxPanel {

From 6a7340e8be3844881b1c114d74687983b9c0ba20 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 18:46:48 +0100
Subject: [PATCH 164/389] Use new upload icon
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/img/upload-big.svg | 20 ++------------------
 1 file changed, 2 insertions(+), 18 deletions(-)

diff --git a/res/img/upload-big.svg b/res/img/upload-big.svg
index 6099c2e976..9a6a265fdb 100644
--- a/res/img/upload-big.svg
+++ b/res/img/upload-big.svg
@@ -1,19 +1,3 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="45px" height="59px" viewBox="-1 -1 45 59" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
-    <!-- Generator: bin/sketchtool 1.4 (305) - http://www.bohemiancoding.com/sketch -->
-    <title>icons_upload_drop</title>
-    <desc>Created with bin/sketchtool.</desc>
-    <defs></defs>
-    <g id="03-Input" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
-        <g id="03_05-File-drop" sketch:type="MSArtboardGroup" transform="translate(-570.000000, -368.000000)">
-            <g id="icons_upload_drop" sketch:type="MSLayerGroup" transform="translate(570.000000, 368.000000)">
-                <g id="Rectangle-5-+-Rectangle-6" sketch:type="MSShapeGroup">
-                    <path d="M0,4.00812931 C0,1.79450062 1.78537926,0 4.00241155,0 L24.8253683,0 C24.8253683,0 42.2466793,16.8210687 42.2466793,16.8210687 L42.2466793,53.000599 C42.2466793,55.2094072 40.4583762,57 38.2531894,57 L3.99348992,57 C1.78794634,57 0,55.1999609 0,52.9918707 L0,4.00812931 Z" id="Rectangle-5" stroke="#76CFA6"></path>
-                    <path d="M40.5848017,19.419576 L29.8354335,19.419576 C26.7387692,19.419576 24.2284269,16.9063989 24.2284269,13.8067771 L24.2284269,4.88501382 L40.5848017,19.419576 Z" id="Rectangle-6-Copy" fill="#FFFFFF"></path>
-                    <path d="M42.2466793,18.3870968 L29.539478,18.3870968 C26.4130381,18.3870968 23.8785579,15.8497544 23.8785579,12.7203286 L23.8785579,0" id="Rectangle-6" stroke="#76CFA6"></path>
-                </g>
-                <path d="M31.3419737,32.9284726 C31.701384,32.9284726 32.0607942,32.8000473 32.3359677,32.5414375 C32.8825707,32.0259772 32.8825707,31.1920926 32.3359677,30.6766323 L21.622922,20.6119619 C21.076319,20.0965016 20.187153,20.0982608 19.638678,20.6102026 L8.9125289,30.6607991 C8.36405391,31.1762594 8.36218198,32.0119032 8.90878504,32.5273635 C9.4553881,33.0445831 10.344554,33.0445831 10.893029,32.530882 L19.2399573,24.7092556 L19.2437012,46.487014 C19.2437012,47.2153435 19.874541,47.8064516 20.6476474,47.8064516 C21.4244976,47.8064516 22.0515936,47.2153435 22.0515936,46.487014 L22.0478497,24.7426814 L30.3498517,32.5414375 C30.6231533,32.8000473 30.9825635,32.9284726 31.3419737,32.9284726 L31.3419737,32.9284726 Z" id="Fill-75" fill="#76CFA6" sketch:type="MSShapeGroup"></path>
-            </g>
-        </g>
-    </g>
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0ZM17.2511 6.97409C16.9775 6.68236 16.5885 6.50012 16.157 6.50012C15.793 6.50012 15.4593 6.62973 15.1996 6.84532C15.1545 6.88267 15.1115 6.92281 15.0707 6.96564L8.79618 13.5539C8.22485 14.1538 8.24801 15.1032 8.8479 15.6746C9.4478 16.2459 10.3973 16.2227 10.9686 15.6228L14.657 11.7501V23.0589C14.657 23.8874 15.3285 24.5589 16.157 24.5589C16.9854 24.5589 17.657 23.8874 17.657 23.0589V11.7502L21.3452 15.6228C21.9165 16.2227 22.866 16.2459 23.4659 15.6746C24.0658 15.1032 24.0889 14.1538 23.5176 13.5539L17.2511 6.97409Z" fill="#0DBD8B"/>
 </svg>

From 1c48804d96c3cdda150e38e2d520a2268dd5728e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 19:28:08 +0100
Subject: [PATCH 165/389] Remove unnecessary class
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss       | 4 ----
 src/components/views/rooms/AuxPanel.tsx | 2 +-
 2 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 8ba31fac20..28d8d1e196 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -40,10 +40,6 @@ limitations under the License.
     align-items: center;
 }
 
-.mx_RoomView_fileDropTargetLabel {
-    position: absolute;
-}
-
 .mx_RoomView_auxPanel {
     min-width: 0px;
     width: 100%;
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 7966643084..543a50d59f 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -156,7 +156,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
         if (this.props.draggingFile) {
             fileDropTarget = (
                 <div className="mx_RoomView_fileDropTarget">
-                    <div className="mx_RoomView_fileDropTargetLabel" title={_t("Drop File Here")}>
+                    <div title={_t("Drop File Here")}>
                         <TintableSvg src={require("../../../../res/img/upload-big.svg")} width="45" height="59" />
                         <br />
                         { _t("Drop file here to upload") }

From 43e1144ae7ca7eff08c1666b4a179ba828d41432 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 19:36:55 +0100
Subject: [PATCH 166/389] Don't use TintableSVG
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This seemed to have caused a little lag and it was unnecessary

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/AuxPanel.tsx | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 543a50d59f..c9150d588f 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -150,14 +150,12 @@ export default class AuxPanel extends React.Component<IProps, IState> {
     }
 
     render() {
-        const TintableSvg = sdk.getComponent("elements.TintableSvg");
-
         let fileDropTarget = null;
         if (this.props.draggingFile) {
             fileDropTarget = (
                 <div className="mx_RoomView_fileDropTarget">
                     <div title={_t("Drop File Here")}>
-                        <TintableSvg src={require("../../../../res/img/upload-big.svg")} width="45" height="59" />
+                        <img src={require( "../../../../res/img/upload-big.svg")}> </img>
                         <br />
                         { _t("Drop file here to upload") }
                     </div>

From 7277c285a9326928555e05766ee3ce33603de18d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 20:10:38 +0100
Subject: [PATCH 167/389] Fix weird crash
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/AuxPanel.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index c9150d588f..cc3408476c 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -155,7 +155,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
             fileDropTarget = (
                 <div className="mx_RoomView_fileDropTarget">
                     <div title={_t("Drop File Here")}>
-                        <img src={require( "../../../../res/img/upload-big.svg")}> </img>
+                        <img src={require( "../../../../res/img/upload-big.svg")} />
                         <br />
                         { _t("Drop file here to upload") }
                     </div>

From 49ea9a4788243346b20fcf5b4b79f46a7c3a80ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 25 Feb 2021 20:10:58 +0100
Subject: [PATCH 168/389] Remove sdk import
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/AuxPanel.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index cc3408476c..59ea8e237a 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -17,7 +17,6 @@ limitations under the License.
 import React from 'react';
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import { Room } from 'matrix-js-sdk/src/models/room'
-import * as sdk from '../../../index';
 import dis from "../../../dispatcher/dispatcher";
 import * as ObjectUtils from '../../../ObjectUtils';
 import AppsDrawer from './AppsDrawer';

From dba52fb5b1411fcc03e2df5db85f1f94601a0337 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Thu, 25 Feb 2021 16:01:46 -0500
Subject: [PATCH 169/389] Autocomplete invited users

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/autocomplete/UserProvider.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx
index 32eea55b0b..7fc01daef9 100644
--- a/src/autocomplete/UserProvider.tsx
+++ b/src/autocomplete/UserProvider.tsx
@@ -155,6 +155,7 @@ export default class UserProvider extends AutocompleteProvider {
 
         const currentUserId = MatrixClientPeg.get().credentials.userId;
         this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId);
+        this.users = this.users.concat(this.room.getMembersWithMembership("invite"));
 
         this.users = sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);
 

From 1a7f9091b4fc41eac6d5cc1b71d1dbe61f5d7f0f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 07:51:03 +0100
Subject: [PATCH 170/389] Animate icon size
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss       | 12 ++++++++++++
 src/components/views/rooms/AuxPanel.tsx | 10 +++++-----
 2 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 28d8d1e196..d5caee5e8b 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -36,10 +36,22 @@ limitations under the License.
     z-index: 3000;
 
     display: flex;
+    flex-direction: column;
     justify-content: center;
     align-items: center;
 }
 
+@keyframes mx_RoomView_fileDropTarget_image_animation {
+  from {width: 0px;}
+  to {width: 32px;}
+}
+
+.mx_RoomView_fileDropTarget_image {
+    animation: mx_RoomView_fileDropTarget_image_animation;
+    animation-duration: 0.5s;
+    margin-bottom: 16px;
+}
+
 .mx_RoomView_auxPanel {
     min-width: 0px;
     width: 100%;
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 59ea8e237a..b3ef8c3cc8 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -153,11 +153,11 @@ export default class AuxPanel extends React.Component<IProps, IState> {
         if (this.props.draggingFile) {
             fileDropTarget = (
                 <div className="mx_RoomView_fileDropTarget">
-                    <div title={_t("Drop File Here")}>
-                        <img src={require( "../../../../res/img/upload-big.svg")} />
-                        <br />
-                        { _t("Drop file here to upload") }
-                    </div>
+                    <img
+                        src={require( "../../../../res/img/upload-big.svg")}
+                        className="mx_RoomView_fileDropTarget_image"
+                    />
+                    { _t("Drop file here to upload") }
                 </div>
             );
         }

From f0c26846c75559e5be013eb25ebe5f54b4f6e264 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 08:11:58 +0100
Subject: [PATCH 171/389] Fix formatting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index d5caee5e8b..2c3fb2b32b 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -42,8 +42,8 @@ limitations under the License.
 }
 
 @keyframes mx_RoomView_fileDropTarget_image_animation {
-  from {width: 0px;}
-  to {width: 32px;}
+    from {width: 0px;}
+    to {width: 32px;}
 }
 
 .mx_RoomView_fileDropTarget_image {

From 172cc01f7d3dcba08235a621cff118efe3e76d83 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 08:12:10 +0100
Subject: [PATCH 172/389] Add background animation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 2c3fb2b32b..5870e107c6 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -20,6 +20,12 @@ limitations under the License.
     flex-direction: column;
 }
 
+
+@keyframes mx_RoomView_fileDropTarget_animation {
+    from {opacity: 0;}
+    to {opacity: 0.95;}
+}
+
 .mx_RoomView_fileDropTarget {
     min-width: 0px;
     width: 100%;
@@ -30,7 +36,8 @@ limitations under the License.
 
     pointer-events: none;
 
-    background-color: $droptarget-bg-color;
+    background-color: $primary-bg-color;
+    opacity: 0.95;
 
     position: absolute;
     z-index: 3000;
@@ -39,6 +46,9 @@ limitations under the License.
     flex-direction: column;
     justify-content: center;
     align-items: center;
+
+    animation: mx_RoomView_fileDropTarget_animation;
+    animation-duration: 0.5s;
 }
 
 @keyframes mx_RoomView_fileDropTarget_image_animation {

From 3e0558f4d97bc618cc8e6d71f411370e894d642f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 08:12:38 +0100
Subject: [PATCH 173/389] Remove droptarget colors
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/themes/dark/css/_dark.scss   | 3 ---
 res/themes/light/css/_light.scss | 3 ---
 2 files changed, 6 deletions(-)

diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index f6f415ce70..a878aa3cdd 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -42,9 +42,6 @@ $preview-bar-bg-color: $header-panel-bg-color;
 $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
 $inverted-bg-color: $base-color;
 
-// used by RoomDropTarget
-$droptarget-bg-color: rgba(21,25,30,0.95);
-
 // used by AddressSelector
 $selected-color: $room-highlight-color;
 
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index ea7b0472e0..c92e491ca2 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -67,9 +67,6 @@ $groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77);
 // used by RoomDirectory permissions
 $plinth-bg-color: $secondary-accent-color;
 
-// used by RoomDropTarget
-$droptarget-bg-color: rgba(255,255,255,0.95);
-
 // used by AddressSelector
 $selected-color: $secondary-accent-color;
 

From 49ea83edb93dfa0e6f572aeac002adaaa0370fc3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 08:14:27 +0100
Subject: [PATCH 174/389] i18n
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/i18n/strings/en_EN.json | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5bbbdf60b5..9af8ccc172 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1381,7 +1381,6 @@
     "Remove %(phone)s?": "Remove %(phone)s?",
     "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
     "Phone Number": "Phone Number",
-    "Drop File Here": "Drop File Here",
     "Drop file here to upload": "Drop file here to upload",
     "This user has not verified all of their sessions.": "This user has not verified all of their sessions.",
     "You have not verified this user.": "You have not verified this user.",

From e90ae2ea7596bff850cf4014c1109f93234132b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 08:18:05 +0100
Subject: [PATCH 175/389] Delint
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 5870e107c6..5e8d84ff32 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -22,8 +22,12 @@ limitations under the License.
 
 
 @keyframes mx_RoomView_fileDropTarget_animation {
-    from {opacity: 0;}
-    to {opacity: 0.95;}
+    from {
+        opacity: 0;
+    }
+    to {
+        opacity: 0.95;
+    }
 }
 
 .mx_RoomView_fileDropTarget {
@@ -52,8 +56,12 @@ limitations under the License.
 }
 
 @keyframes mx_RoomView_fileDropTarget_image_animation {
-    from {width: 0px;}
-    to {width: 32px;}
+    from {
+        width: 0px;
+    }
+    to {
+        width: 32px;
+    }
 }
 
 .mx_RoomView_fileDropTarget_image {

From 819a0b013fda927bea3197e458741f4f4f85a271 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 11:08:31 +0100
Subject: [PATCH 176/389] min-width
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

We need to allow the container to be smaller

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_RoomView.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 5e8d84ff32..28591ad7a4 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -140,6 +140,7 @@ limitations under the License.
     display: flex;
     flex-direction: column;
     width: 100%;
+    min-width: 0;
 }
 
 .mx_RoomView_body {

From 0d6a9fce67d24f441a62e140a2a73f669b959cbc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 11:12:14 +0100
Subject: [PATCH 177/389] Remove weird styling
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 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 ff09af454e..42eafe5bdc 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -2058,7 +2058,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                             appsShown={this.state.showApps}
                         />
                         <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
-                            <div className={"mx_RoomView_container"}>
+                            <div className="mx_RoomView_container">
                                 {auxPanel}
                                 <div className="mx_RoomView_body">
                                     <div className={timelineClasses}>

From 11c5aa02d290739fff31bfa8365fe76562032594 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 11:19:45 +0100
Subject: [PATCH 178/389] Remove mx_RoomView_container
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_MainSplit.scss     |  1 +
 res/css/structures/_RoomView.scss      |  8 -------
 src/components/structures/RoomView.tsx | 30 ++++++++++++--------------
 3 files changed, 15 insertions(+), 24 deletions(-)

diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss
index 9597083e9c..5fa62e921d 100644
--- a/res/css/structures/_MainSplit.scss
+++ b/res/css/structures/_MainSplit.scss
@@ -20,6 +20,7 @@ limitations under the License.
     min-width: 0;
     height: 100%;
     justify-content: space-between;
+    min-height: 0;
 }
 
 .mx_MainSplit > .mx_RightPanel_ResizeWrapper {
diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 28591ad7a4..b3dab5f992 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -135,14 +135,6 @@ limitations under the License.
     height: 50px;
 }
 
-.mx_RoomView_container {
-    position: relative; //for .mx_RoomView_auxPanel_fullHeight
-    display: flex;
-    flex-direction: column;
-    width: 100%;
-    min-width: 0;
-}
-
 .mx_RoomView_body {
     display: flex;
     flex-direction: column;
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 42eafe5bdc..be2f81a176 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -2058,24 +2058,22 @@ export default class RoomView extends React.Component<IProps, IState> {
                             appsShown={this.state.showApps}
                         />
                         <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
-                            <div className="mx_RoomView_container">
+                            <div className="mx_RoomView_body">
                                 {auxPanel}
-                                <div className="mx_RoomView_body">
-                                    <div className={timelineClasses}>
-                                        {topUnreadMessagesBar}
-                                        {jumpToBottom}
-                                        {messagePanel}
-                                        {searchResultsPanel}
-                                    </div>
-                                    <div className={statusBarAreaClass}>
-                                        <div className="mx_RoomView_statusAreaBox">
-                                            <div className="mx_RoomView_statusAreaBox_line" />
-                                            {statusBar}
-                                        </div>
-                                    </div>
-                                    {previewBar}
-                                    {messageComposer}
+                                <div className={timelineClasses}>
+                                    {topUnreadMessagesBar}
+                                    {jumpToBottom}
+                                    {messagePanel}
+                                    {searchResultsPanel}
                                 </div>
+                                <div className={statusBarAreaClass}>
+                                    <div className="mx_RoomView_statusAreaBox">
+                                        <div className="mx_RoomView_statusAreaBox_line" />
+                                        {statusBar}
+                                    </div>
+                                </div>
+                                {previewBar}
+                                {messageComposer}
                             </div>
                         </MainSplit>
                     </ErrorBoundary>

From 9a5ba072ba0f551dc618cdf8bfb7afba004d01fe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 11:23:58 +0100
Subject: [PATCH 179/389] Fix auxPanel
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 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 be2f81a176..5b79f23e0b 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -2059,8 +2059,8 @@ export default class RoomView extends React.Component<IProps, IState> {
                         />
                         <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
                             <div className="mx_RoomView_body">
-                                {auxPanel}
                                 <div className={timelineClasses}>
+                                    {auxPanel}
                                     {topUnreadMessagesBar}
                                     {jumpToBottom}
                                     {messagePanel}

From 3bed37463fea1ff1a7c86ef5fe9a0a123e06008f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 11:38:05 +0100
Subject: [PATCH 180/389] Remove unnecessary code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_MainSplit.scss | 1 -
 1 file changed, 1 deletion(-)

diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss
index 5fa62e921d..2d9ea2729c 100644
--- a/res/css/structures/_MainSplit.scss
+++ b/res/css/structures/_MainSplit.scss
@@ -19,7 +19,6 @@ limitations under the License.
     flex-direction: row;
     min-width: 0;
     height: 100%;
-    justify-content: space-between;
     min-height: 0;
 }
 

From f21aedc6cf38d741de183fa1d158d7f7de673699 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 26 Feb 2021 10:23:09 +0000
Subject: [PATCH 181/389] Add Space Panel with Room List filtering

---
 res/css/_components.scss                      |   1 +
 res/css/structures/_LeftPanel.scss            |  22 +-
 res/css/structures/_SpacePanel.scss           | 237 ++++++++++++++++++
 res/img/element-icons/expand-space-panel.svg  |   4 +
 res/themes/light/css/_mods.scss               |   4 +
 src/components/structures/LeftPanel.tsx       |  23 +-
 src/components/views/avatars/RoomAvatar.tsx   |   2 +-
 src/components/views/spaces/SpacePanel.tsx    | 212 ++++++++++++++++
 .../views/spaces/SpaceTreeLevel.tsx           | 184 ++++++++++++++
 src/i18n/strings/en_EN.json                   |   4 +-
 src/stores/room-list/RoomListStore.ts         |  23 +-
 src/stores/room-list/SpaceWatcher.ts          |  39 +++
 src/stores/room-list/algorithms/Algorithm.ts  |   3 +
 .../room-list/filters/SpaceFilterCondition.ts |  69 +++++
 14 files changed, 796 insertions(+), 31 deletions(-)
 create mode 100644 res/css/structures/_SpacePanel.scss
 create mode 100644 res/img/element-icons/expand-space-panel.svg
 create mode 100644 src/components/views/spaces/SpacePanel.tsx
 create mode 100644 src/components/views/spaces/SpaceTreeLevel.tsx
 create mode 100644 src/stores/room-list/SpaceWatcher.ts
 create mode 100644 src/stores/room-list/filters/SpaceFilterCondition.ts

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 006bac09c9..29b5262826 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -27,6 +27,7 @@
 @import "./structures/_RoomView.scss";
 @import "./structures/_ScrollPanel.scss";
 @import "./structures/_SearchBox.scss";
+@import "./structures/_SpacePanel.scss";
 @import "./structures/_TabbedView.scss";
 @import "./structures/_ToastContainer.scss";
 @import "./structures/_UploadBar.scss";
diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss
index 168590502d..f1f27014ee 100644
--- a/res/css/structures/_LeftPanel.scss
+++ b/res/css/structures/_LeftPanel.scss
@@ -15,6 +15,7 @@ limitations under the License.
 */
 
 $groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
+$roomListCollapsedWidth: 68px;
 
 .mx_LeftPanel {
     background-color: $roomlist-bg-color;
@@ -37,18 +38,12 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
         // GroupFilterPanel handles its own CSS
     }
 
-    &:not(.mx_LeftPanel_hasGroupFilterPanel) {
-        .mx_LeftPanel_roomListContainer {
-            width: 100%;
-        }
-    }
-
     // Note: The 'room list' in this context is actually everything that isn't the tag
     // panel, such as the menu options, breadcrumbs, filtering, etc
     .mx_LeftPanel_roomListContainer {
-        width: calc(100% - $groupFilterPanelWidth);
         background-color: $roomlist-bg-color;
-
+        flex: 1 0 0;
+        min-width: 0;
         // Create another flexbox (this time a column) for the room list components
         display: flex;
         flex-direction: column;
@@ -168,17 +163,10 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
     // These styles override the defaults for the minimized (66px) layout
     &.mx_LeftPanel_minimized {
         min-width: unset;
-
-        // We have to forcefully set the width to override the resizer's style attribute.
-        &.mx_LeftPanel_hasGroupFilterPanel {
-            width: calc(68px + $groupFilterPanelWidth) !important;
-        }
-        &:not(.mx_LeftPanel_hasGroupFilterPanel) {
-            width: 68px !important;
-        }
+        width: unset !important;
 
         .mx_LeftPanel_roomListContainer {
-            width: 68px;
+            width: $roomListCollapsedWidth;
 
             .mx_LeftPanel_userHeader {
                 flex-direction: row;
diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss
new file mode 100644
index 0000000000..8de85f95ef
--- /dev/null
+++ b/res/css/structures/_SpacePanel.scss
@@ -0,0 +1,237 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+
+
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+$topLevelHeight: 32px;
+$nestedHeight: 24px;
+$gutterSize: 21px;
+$activeStripeSize: 4px;
+
+.mx_SpacePanel {
+    flex: 0 0 auto;
+    background-color: $groupFilterPanel-bg-color;
+    padding: 0;
+    margin: 0;
+
+    // Create another flexbox so the Panel fills the container
+    display: flex;
+    flex-direction: column;
+    overflow-y: auto;
+
+    .mx_SpacePanel_spaceTreeWrapper {
+        flex: 1;
+    }
+
+    .mx_SpacePanel_toggleCollapse {
+        flex: 0 0 auto;
+        width: 40px;
+        height: 40px;
+        mask-position: center;
+        mask-size: 32px;
+        mask-repeat: no-repeat;
+        margin-left: $gutterSize;
+        margin-bottom: 12px;
+        background-color: $roomlist-header-color;
+        mask-image: url('$(res)/img/element-icons/expand-space-panel.svg');
+
+        &.expanded {
+            transform: scaleX(-1);
+        }
+    }
+
+    ul {
+        margin: 0;
+        list-style: none;
+        padding: 0;
+        padding-left: 16px;
+    }
+
+    .mx_AutoHideScrollbar {
+        padding: 16px 12px 16px 0;
+    }
+
+    .mx_SpaceButton_toggleCollapse {
+        cursor: pointer;
+    }
+
+    .mx_SpaceItem {
+        position: relative;
+    }
+
+    .mx_SpaceItem.collapsed {
+        & > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse {
+            transform: rotate(-90deg);
+        }
+
+        & > .mx_SpaceTreeLevel {
+            display: none;
+        }
+    }
+
+    .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
+        margin-left: $gutterSize;
+
+        &.mx_SpaceButton_active {
+            &::before {
+                left: -$gutterSize;
+            }
+        }
+    }
+
+    .mx_SpaceButton {
+        border-radius: 8px;
+        position: relative;
+        margin-bottom: 16px;
+        display: flex;
+        align-items: center;
+
+        .mx_SpaceButton_name {
+            flex: 1;
+            margin-left: 8px;
+            white-space: nowrap;
+            display: block;
+            max-width: 150px;
+            text-overflow: ellipsis;
+            overflow: hidden;
+
+            font-size: $font-14px;
+            line-height: $font-18px;
+        }
+
+        .mx_SpaceButton_toggleCollapse {
+            width: calc($gutterSize - $activeStripeSize);
+            margin-left: 1px;
+            height: 20px;
+            mask-position: center;
+            mask-size: 20px;
+            mask-repeat: no-repeat;
+            background-color: $roomlist-header-color;
+            mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
+        }
+
+        &.mx_SpaceButton_active {
+            &::before {
+                position: absolute;
+                content: '';
+                width: $activeStripeSize;
+                top: 0;
+                left: 0;
+                bottom: 0;
+                background-color: $accent-color;
+                border-radius: 0 4px 4px 0;
+            }
+        }
+
+        .mx_SpaceButton_avatarPlaceholder {
+            width: $topLevelHeight;
+            min-width: $topLevelHeight;
+            height: $topLevelHeight;
+            border-radius: 8px;
+
+            &::before {
+                position: absolute;
+                content: '';
+                width: $topLevelHeight;
+                height: $topLevelHeight;
+                top: 0;
+                left: 0;
+                mask-position: center;
+                mask-repeat: no-repeat;
+                mask-size: 18px;
+            }
+        }
+
+        &.mx_SpaceButton_home .mx_SpaceButton_avatarPlaceholder {
+            background-color: #ffffff;
+
+            &::before {
+                background-color: #3f3d3d;
+                mask-image: url('$(res)/img/element-icons/home.svg');
+            }
+        }
+
+        &.mx_SpaceButton_newCancel .mx_SpaceButton_avatarPlaceholder {
+            background-color: $icon-button-color;
+
+            &::before {
+                transform: rotate(45deg);
+            }
+        }
+
+        .mx_BaseAvatar_image {
+            border-radius: 8px;
+        }
+    }
+
+    .mx_SpacePanel_badgeContainer {
+        height: 16px;
+        // don't set width so that it takes no space when there is no badge to show
+        margin: auto 0; // vertically align
+
+        // Create a flexbox to make aligning dot badges easier
+        display: flex;
+        align-items: center;
+
+        .mx_NotificationBadge {
+            margin: 0 2px; // centering
+        }
+
+        .mx_NotificationBadge_dot {
+            // make the smaller dot occupy the same width for centering
+            margin-left: 7px;
+            margin-right: 7px;
+        }
+    }
+
+    &.collapsed {
+        .mx_SpaceButton {
+            .mx_SpacePanel_badgeContainer {
+                position: absolute;
+                right: -8px;
+                top: -4px;
+            }
+        }
+    }
+
+    &:not(.collapsed) {
+        .mx_SpaceButton:hover,
+        .mx_SpaceButton:focus-within,
+        .mx_SpaceButton_hasMenuOpen {
+            // Hide the badge container on hover because it'll be a menu button
+            .mx_SpacePanel_badgeContainer {
+                width: 0;
+                height: 0;
+                display: none;
+            }
+        }
+    }
+
+    /* root space buttons are bigger and not indented */
+    & > .mx_AutoHideScrollbar {
+        & > .mx_SpaceButton {
+            height: $topLevelHeight;
+
+            &.mx_SpaceButton_active::before {
+                height: $topLevelHeight;
+            }
+        }
+
+        & > ul {
+            padding-left: 0;
+        }
+    }
+}
diff --git a/res/img/element-icons/expand-space-panel.svg b/res/img/element-icons/expand-space-panel.svg
new file mode 100644
index 0000000000..11232acd58
--- /dev/null
+++ b/res/img/element-icons/expand-space-panel.svg
@@ -0,0 +1,4 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7701 16.617H22.3721L18.614 20.3751C18.3137 20.6754 18.3137 21.1683 18.614 21.4686C18.9143 21.769 19.3995 21.769 19.6998 21.4686L24.7747 16.3937C25.0751 16.0934 25.0751 15.6082 24.7747 15.3079L19.7075 10.2253C19.4072 9.92492 18.922 9.92492 18.6217 10.2253C18.3214 10.5256 18.3214 11.0107 18.6217 11.3111L22.3721 15.0768H13.7701C13.3465 15.0768 13 15.4234 13 15.8469C13 16.2705 13.3465 16.617 13.7701 16.617Z" fill="#86888A"/>
+<rect x="7" y="10" width="1.5" height="12" rx="0.75" fill="#86888A"/>
+</svg>
diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss
index 30aaeedf8f..fbca58dfb1 100644
--- a/res/themes/light/css/_mods.scss
+++ b/res/themes/light/css/_mods.scss
@@ -16,6 +16,10 @@
         backdrop-filter: blur($groupFilterPanel-background-blur-amount);
     }
 
+    .mx_SpacePanel {
+        backdrop-filter: blur($groupFilterPanel-background-blur-amount);
+    }
+
     .mx_LeftPanel .mx_LeftPanel_roomListContainer {
         backdrop-filter: blur($roomlist-background-blur-amount);
     }
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index 4445ff3ff8..82dd9443cc 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -39,6 +39,7 @@ import { OwnProfileStore } from "../../stores/OwnProfileStore";
 import { MatrixClientPeg } from "../../MatrixClientPeg";
 import RoomListNumResults from "../views/rooms/RoomListNumResults";
 import LeftPanelWidget from "./LeftPanelWidget";
+import SpacePanel from "../views/spaces/SpacePanel";
 
 interface IProps {
     isMinimized: boolean;
@@ -388,12 +389,19 @@ export default class LeftPanel extends React.Component<IProps, IState> {
     }
 
     public render(): React.ReactNode {
-        const groupFilterPanel = !this.state.showGroupFilterPanel ? null : (
-            <div className="mx_LeftPanel_GroupFilterPanelContainer">
-                <GroupFilterPanel />
-                {SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
-            </div>
-        );
+        let leftLeftPanel;
+        // Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now
+        // ignore it and force the rendering of SpacePanel if that Labs flag is enabled.
+        if (SettingsStore.getValue("feature_spaces")) {
+            leftLeftPanel = <SpacePanel />;
+        } else if (this.state.showGroupFilterPanel) {
+            leftLeftPanel = (
+                <div className="mx_LeftPanel_GroupFilterPanelContainer">
+                    <GroupFilterPanel />
+                    {SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
+                </div>
+            );
+        }
 
         const roomList = <RoomList
             onKeyDown={this.onKeyDown}
@@ -406,7 +414,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 
         const containerClasses = classNames({
             "mx_LeftPanel": true,
-            "mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel,
             "mx_LeftPanel_minimized": this.props.isMinimized,
         });
 
@@ -417,7 +424,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 
         return (
             <div className={containerClasses}>
-                {groupFilterPanel}
+                {leftLeftPanel}
                 <aside className="mx_LeftPanel_roomListContainer">
                     {this.renderHeader()}
                     {this.renderSearchExplore()}
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 0e16d17da9..952b9d4cb6 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -24,7 +24,7 @@ import Modal from '../../../Modal';
 import * as Avatar from '../../../Avatar';
 import {ResizeMethod} from "../../../Avatar";
 
-interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick">{
+interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
     // Room may be left unset here, but if it is,
     // oobData.avatarUrl should be set (else there
     // would be nowhere to get the avatar from)
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
new file mode 100644
index 0000000000..bc9cd5c9fd
--- /dev/null
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -0,0 +1,212 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useState} from "react";
+import classNames from "classnames";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import {_t} from "../../../languageHandler";
+import RoomAvatar from "../avatars/RoomAvatar";
+import {SpaceItem} from "./SpaceTreeLevel";
+import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {useEventEmitter} from "../../../hooks/useEventEmitter";
+import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore";
+import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
+import NotificationBadge from "../rooms/NotificationBadge";
+import {
+    RovingAccessibleButton,
+    RovingAccessibleTooltipButton,
+    RovingTabIndexProvider,
+} from "../../../accessibility/RovingTabIndex";
+import {Key} from "../../../Keyboard";
+
+interface IButtonProps {
+    space?: Room;
+    className?: string;
+    selected?: boolean;
+    tooltip?: string;
+    notificationState?: SpaceNotificationState;
+    isNarrow?: boolean;
+    onClick(): void;
+}
+
+const SpaceButton: React.FC<IButtonProps> = ({
+    space,
+    className,
+    selected,
+    onClick,
+    tooltip,
+    notificationState,
+    isNarrow,
+    children,
+}) => {
+    const classes = classNames("mx_SpaceButton", className, {
+        mx_SpaceButton_active: selected,
+    });
+
+    let avatar = <div className="mx_SpaceButton_avatarPlaceholder" />;
+    if (space) {
+        avatar = <RoomAvatar width={32} height={32} room={space} />;
+    }
+
+    let notifBadge;
+    if (notificationState) {
+        notifBadge = <div className="mx_SpacePanel_badgeContainer">
+            <NotificationBadge forceCount={false} notification={notificationState} />
+        </div>;
+    }
+
+    let button;
+    if (isNarrow) {
+        button = (
+            <RovingAccessibleTooltipButton className={classes} title={tooltip} onClick={onClick} role="treeitem">
+                { avatar }
+                { notifBadge }
+                { children }
+            </RovingAccessibleTooltipButton>
+        );
+    } else {
+        button = (
+            <RovingAccessibleButton className={classes} onClick={onClick} role="treeitem">
+                { avatar }
+                <span className="mx_SpaceButton_name">{ tooltip }</span>
+                { notifBadge }
+                { children }
+            </RovingAccessibleButton>
+        );
+    }
+
+    return <li className={classNames({
+        "mx_SpaceItem": true,
+        "collapsed": isNarrow,
+    })}>
+        { button }
+    </li>;
+}
+
+const useSpaces = (): [Room[], Room | null] => {
+    const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
+    useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
+    const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
+    useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
+    return [spaces, activeSpace];
+};
+
+const SpacePanel = () => {
+    const [spaces, activeSpace] = useSpaces();
+    const [isPanelCollapsed, setPanelCollapsed] = useState(true);
+
+    const onKeyDown = (ev: React.KeyboardEvent) => {
+        let handled = true;
+
+        switch (ev.key) {
+            case Key.ARROW_UP:
+                onMoveFocus(ev.target as Element, true);
+                break;
+            case Key.ARROW_DOWN:
+                onMoveFocus(ev.target as Element, false);
+                break;
+            default:
+                handled = false;
+        }
+
+        if (handled) {
+            // consume all other keys in context menu
+            ev.stopPropagation();
+            ev.preventDefault();
+        }
+    };
+
+    const onMoveFocus = (element: Element, up: boolean) => {
+        let descending = false; // are we currently descending or ascending through the DOM tree?
+        let classes: DOMTokenList;
+
+        do {
+            const child = up ? element.lastElementChild : element.firstElementChild;
+            const sibling = up ? element.previousElementSibling : element.nextElementSibling;
+
+            if (descending) {
+                if (child) {
+                    element = child;
+                } else if (sibling) {
+                    element = sibling;
+                } else {
+                    descending = false;
+                    element = element.parentElement;
+                }
+            } else {
+                if (sibling) {
+                    element = sibling;
+                    descending = true;
+                } else {
+                    element = element.parentElement;
+                }
+            }
+
+            if (element) {
+                if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
+                    element = up ? element.lastElementChild : element.firstElementChild;
+                    descending = true;
+                }
+                classes = element.classList;
+            }
+        } while (element && !classes.contains("mx_SpaceButton"));
+
+        if (element) {
+            (element as HTMLElement).focus();
+        }
+    };
+
+    const activeSpaces = activeSpace ? [activeSpace] : [];
+    const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
+    // TODO drag and drop for re-arranging order
+    return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
+        {({onKeyDownHandler}) => (
+            <ul
+                className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
+                onKeyDown={onKeyDownHandler}
+            >
+                <AutoHideScrollbar className="mx_SpacePanel_spaceTreeWrapper">
+                    <div className="mx_SpaceTreeLevel">
+                        <SpaceButton
+                            className="mx_SpaceButton_home"
+                            onClick={() => SpaceStore.instance.setActiveSpace(null)}
+                            selected={!activeSpace}
+                            tooltip={_t("Home")}
+                            notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
+                            isNarrow={isPanelCollapsed}
+                        />
+                        { spaces.map(s => <SpaceItem
+                            key={s.roomId}
+                            space={s}
+                            activeSpaces={activeSpaces}
+                            isPanelCollapsed={isPanelCollapsed}
+                            onExpand={() => setPanelCollapsed(false)}
+                        />) }
+                    </div>
+                </AutoHideScrollbar>
+                <AccessibleTooltipButton
+                    className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
+                    onClick={evt => setPanelCollapsed(!isPanelCollapsed)}
+                    title={expandCollapseButtonTitle}
+                />
+            </ul>
+        )}
+    </RovingTabIndexProvider>
+};
+
+export default SpacePanel;
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
new file mode 100644
index 0000000000..14fe68ff66
--- /dev/null
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -0,0 +1,184 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import classNames from "classnames";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import RoomAvatar from "../avatars/RoomAvatar";
+import SpaceStore from "../../../stores/SpaceStore";
+import NotificationBadge from "../rooms/NotificationBadge";
+import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
+import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+
+interface IItemProps {
+    space?: Room;
+    activeSpaces: Room[];
+    isNested?: boolean;
+    isPanelCollapsed?: boolean;
+    onExpand?: Function;
+}
+
+interface IItemState {
+    collapsed: boolean;
+    contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
+}
+
+export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
+    static contextType = MatrixClientContext;
+
+    constructor(props) {
+        super(props);
+
+        this.state = {
+            collapsed: !props.isNested,   // default to collapsed for root items
+            contextMenuPosition: null,
+        };
+    }
+
+    private toggleCollapse(evt) {
+        if (this.props.onExpand && this.state.collapsed) {
+            this.props.onExpand();
+        }
+        this.setState({collapsed: !this.state.collapsed});
+        // don't bubble up so encapsulating button for space
+        // doesn't get triggered
+        evt.stopPropagation();
+    }
+
+    private onContextMenu = (ev: React.MouseEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+        this.setState({
+            contextMenuPosition: {
+                right: ev.clientX,
+                top: ev.clientY,
+                height: 0,
+            },
+        });
+    }
+
+    private onClick = (ev: React.MouseEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+        SpaceStore.instance.setActiveSpace(this.props.space);
+    };
+
+    render() {
+        const {space, activeSpaces, isNested} = this.props;
+
+        const forceCollapsed = this.props.isPanelCollapsed;
+        const isNarrow = this.props.isPanelCollapsed;
+        const collapsed = this.state.collapsed || forceCollapsed;
+
+        const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId);
+        const isActive = activeSpaces.includes(space);
+        const itemClasses = classNames({
+            "mx_SpaceItem": true,
+            "collapsed": collapsed,
+            "hasSubSpaces": childSpaces && childSpaces.length,
+        });
+        const classes = classNames("mx_SpaceButton", {
+            mx_SpaceButton_active: isActive,
+            mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
+        });
+        const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
+        const childItems = childSpaces && !collapsed ? <SpaceTreeLevel
+            spaces={childSpaces}
+            activeSpaces={activeSpaces}
+            isNested={true}
+        /> : null;
+        let notifBadge;
+        if (notificationState) {
+            notifBadge = <div className="mx_SpacePanel_badgeContainer">
+                <NotificationBadge forceCount={false} notification={notificationState} />
+            </div>;
+        }
+
+        const avatarSize = isNested ? 24 : 32;
+
+        const toggleCollapseButton = childSpaces && childSpaces.length ?
+            <button
+                className="mx_SpaceButton_toggleCollapse"
+                onClick={evt => this.toggleCollapse(evt)}
+            /> : null;
+
+        let button;
+        if (isNarrow) {
+            button = (
+                <RovingAccessibleTooltipButton
+                    className={classes}
+                    title={space.name}
+                    onClick={this.onClick}
+                    onContextMenu={this.onContextMenu}
+                    forceHide={!!this.state.contextMenuPosition}
+                    role="treeitem"
+                >
+                    { toggleCollapseButton }
+                    <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
+                    { notifBadge }
+                </RovingAccessibleTooltipButton>
+            );
+        } else {
+            button = (
+                <RovingAccessibleButton
+                    className={classes}
+                    onClick={this.onClick}
+                    onContextMenu={this.onContextMenu}
+                    role="treeitem"
+                >
+                    { toggleCollapseButton }
+                    <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
+                    <span className="mx_SpaceButton_name">{ space.name }</span>
+                    { notifBadge }
+                </RovingAccessibleButton>
+            );
+        }
+
+        return (
+            <li className={itemClasses}>
+                { button }
+                { childItems }
+            </li>
+        );
+    }
+}
+
+interface ITreeLevelProps {
+    spaces: Room[];
+    activeSpaces: Room[];
+    isNested?: boolean;
+}
+
+const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
+    spaces,
+    activeSpaces,
+    isNested,
+}) => {
+    return <ul className="mx_SpaceTreeLevel">
+        {spaces.map(s => {
+            return (<SpaceItem
+                key={s.roomId}
+                activeSpaces={activeSpaces}
+                space={s}
+                isNested={isNested}
+            />);
+        })}
+    </ul>;
+}
+
+export default SpaceTreeLevel;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f443c4961b..c946310496 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -978,6 +978,9 @@
     "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
     "Decline (%(counter)s)": "Decline (%(counter)s)",
     "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
+    "Expand space panel": "Expand space panel",
+    "Collapse space panel": "Collapse space panel",
+    "Home": "Home",
     "Remove": "Remove",
     "Upload": "Upload",
     "This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
@@ -1941,7 +1944,6 @@
     "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",
     "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",
diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts
index 667d9de64d..60a960261c 100644
--- a/src/stores/room-list/RoomListStore.ts
+++ b/src/stores/room-list/RoomListStore.ts
@@ -35,6 +35,7 @@ import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
 import { NameFilterCondition } from "./filters/NameFilterCondition";
 import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
 import { VisibilityProvider } from "./filters/VisibilityProvider";
+import { SpaceWatcher } from "./SpaceWatcher";
 
 interface IState {
     tagsEnabled?: boolean;
@@ -56,7 +57,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
     private initialListsGenerated = false;
     private algorithm = new Algorithm();
     private filterConditions: IFilterCondition[] = [];
-    private tagWatcher = new TagWatcher(this);
+    private tagWatcher: TagWatcher;
+    private spaceWatcher: SpaceWatcher;
     private updateFn = new MarkedExecution(() => {
         for (const tagId of Object.keys(this.orderedLists)) {
             RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.orderedLists[tagId]);
@@ -77,6 +79,15 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
         RoomViewStore.addListener(() => this.handleRVSUpdate({}));
         this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
         this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated);
+        this.setupWatchers();
+    }
+
+    private setupWatchers() {
+        if (SettingsStore.getValue("feature_spaces")) {
+            this.spaceWatcher = new SpaceWatcher(this);
+        } else {
+            this.tagWatcher = new TagWatcher(this);
+        }
     }
 
     public get unfilteredLists(): ITagMap {
@@ -92,9 +103,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
     // Intended for test usage
     public async resetStore() {
         await this.reset();
-        this.tagWatcher = new TagWatcher(this);
         this.filterConditions = [];
         this.initialListsGenerated = false;
+        this.setupWatchers();
 
         this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
         this.algorithm.off(FILTER_CHANGED, this.onAlgorithmListUpdated);
@@ -554,8 +565,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
     public async regenerateAllLists({trigger = true}) {
         console.warn("Regenerating all room lists");
 
-        const rooms = this.matrixClient.getVisibleRooms()
-            .filter(r => VisibilityProvider.instance.isRoomVisible(r));
+        const rooms = [
+            ...this.matrixClient.getVisibleRooms(),
+            // also show space invites in the room list
+            ...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"),
+        ].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/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts
new file mode 100644
index 0000000000..d26f563a91
--- /dev/null
+++ b/src/stores/room-list/SpaceWatcher.ts
@@ -0,0 +1,39 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Room } from "matrix-js-sdk/src/models/room";
+
+import { RoomListStoreClass } from "./RoomListStore";
+import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
+import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
+
+/**
+ * Watches for changes in spaces to manage the filter on the provided RoomListStore
+ */
+export class SpaceWatcher {
+    private filter = new SpaceFilterCondition();
+    private activeSpace: Room = SpaceStore.instance.activeSpace;
+
+    constructor(private store: RoomListStoreClass) {
+        this.filter.updateSpace(this.activeSpace); // get the filter into a consistent state
+        store.addFilter(this.filter);
+        SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
+    }
+
+    private onSelectedSpaceUpdated = (activeSpace) => {
+        this.filter.updateSpace(this.activeSpace = activeSpace);
+    };
+}
diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts
index f709fc3ccb..40fdae5ae4 100644
--- a/src/stores/room-list/algorithms/Algorithm.ts
+++ b/src/stores/room-list/algorithms/Algorithm.ts
@@ -186,6 +186,9 @@ export class Algorithm extends EventEmitter {
     }
 
     private async doUpdateStickyRoom(val: Room) {
+        // no-op sticky rooms
+        if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") val = null;
+
         // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
         // otherwise we risk duplicating rooms.
 
diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts
new file mode 100644
index 0000000000..49c58c9d1d
--- /dev/null
+++ b/src/stores/room-list/filters/SpaceFilterCondition.ts
@@ -0,0 +1,69 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { EventEmitter } from "events";
+import { Room } from "matrix-js-sdk/src/models/room";
+
+import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
+import { IDestroyable } from "../../../utils/IDestroyable";
+import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
+import { setHasDiff } from "../../../utils/sets";
+
+/**
+ * A filter condition for the room list which reveals rooms which
+ * are a member of a given space or if no space is selected shows:
+ *  + Orphaned rooms (ones not in any space you are a part of)
+ *  + All DMs
+ */
+export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
+    private roomIds = new Set<Room>();
+    private space: Room = null;
+
+    public get relativePriority(): FilterPriority {
+        // Lowest priority so we can coarsely find rooms.
+        return FilterPriority.Lowest;
+    }
+
+    public isVisible(room: Room): boolean {
+        return this.roomIds.has(room.roomId);
+    }
+
+    private onStoreUpdate = async (): Promise<void> => {
+        const beforeRoomIds = this.roomIds;
+        this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space);
+
+        if (setHasDiff(beforeRoomIds, this.roomIds)) {
+            // XXX: Room List Store has a bug where rooms which are synced after the filter is set
+            // are excluded from the filter, this is a workaround for it.
+            this.emit(FILTER_CHANGED);
+            setTimeout(() => {
+                this.emit(FILTER_CHANGED);
+            }, 500);
+        }
+    };
+
+    private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
+
+    public updateSpace(space: Room) {
+        SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
+        SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
+        this.onStoreUpdate(); // initial update from the change to the space
+    }
+
+    public destroy(): void {
+        SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
+    }
+}

From 3a643e5b9df189916d2a3b8162636e2d05a5e2d5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 11:46:54 +0100
Subject: [PATCH 182/389] Remove unnecessary changes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/structures/_MainSplit.scss | 34 ++++++++++++------------------
 res/css/structures/_RoomView.scss  |  2 --
 2 files changed, 13 insertions(+), 23 deletions(-)

diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss
index 2d9ea2729c..8199121420 100644
--- a/res/css/structures/_MainSplit.scss
+++ b/res/css/structures/_MainSplit.scss
@@ -18,35 +18,27 @@ limitations under the License.
     display: flex;
     flex-direction: row;
     min-width: 0;
-    height: 100%;
     min-height: 0;
+    height: 100%;
 }
 
 .mx_MainSplit > .mx_RightPanel_ResizeWrapper {
-    padding: 0 5px 5px 0px;
+    padding: 5px;
+    // margin left to not allow the handle to not encroach on the space for the scrollbar
+    margin-left: 8px;
     height: calc(100vh - 51px); // height of .mx_RoomHeader.light-panel
 
-    .mx_RightPanel_ResizeHandle {
-        width: 9px;
-    }
-
     &:hover .mx_RightPanel_ResizeHandle {
-        &::before {
-            position: absolute;
-            left: 6px;
-            top: 50%;
-            transform: translate(0, -50%);
+        // Need to use important to override element style attributes
+        // set by re-resizable
+        top: 50% !important;
+        transform: translate(0, -50%);
 
-            height: 64px;
-            width: 4px;
-            border-radius: 4px;
+        height: 64px !important; // to match width of the ones on roomlist
+        width: 4px !important;
+        border-radius: 4px !important;
 
-            content: ' ';
-
-            background-color: $primary-fg-color;
-            opacity: 0.8;
-
-            margin-left: -10px;
-        }
+        background-color: $primary-fg-color;
+        opacity: 0.8;
     }
 }
diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index b3dab5f992..26382b55e8 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -144,8 +144,6 @@ limitations under the License.
     .mx_RoomView_messagePanel, .mx_RoomView_messagePanelSpinner, .mx_RoomView_messagePanelSearchSpinner {
         order: 2;
     }
-
-    margin-right: 10px;
 }
 
 .mx_RoomView_body .mx_RoomView_timeline {

From b58ce4ccfc6b74be7fe3dacb5affde9ce23d86c9 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 26 Feb 2021 14:00:02 +0000
Subject: [PATCH 183/389] Null check for maxHeight in call view

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

diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 467196a68a..7cac682794 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -518,7 +518,7 @@ export default class CallView extends React.Component<IProps, IState> {
             }
 
             // if we're fullscreen, we don't want to set a maxHeight on the video element.
-            const maxVideoHeight = getFullScreenElement() ? null : (
+            const maxVideoHeight = getFullScreenElement() || !this.props.maxVideoHeight ? null : (
                 this.props.maxVideoHeight - (HEADER_HEIGHT + BOTTOM_PADDING + BOTTOM_MARGIN_TOP_BOTTOM)
             );
             contentView = <div className={containerClasses}

From 90af6ddccefdecb49006e7e273352b6edce6294a Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 26 Feb 2021 14:48:18 +0000
Subject: [PATCH 184/389] Log when turn creds expire

Which, due to how special the js-sdk API is, needs to be done accross
two different projects.
---
 src/CallHandler.tsx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index ab3a601d93..42a38c7a54 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -629,6 +629,8 @@ export default class CallHandler {
         const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId;
         logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
 
+        const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
+        console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " seconds");
         const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
 
         this.calls.set(roomId, call);

From a0200de7b4a0afdecfdf5f0e06a68e0d990876f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 18:34:54 +0100
Subject: [PATCH 185/389] Add scrollToBottomOnMessageSent setting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/settings/Settings.ts | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index ca5e2f1d04..dd431f9b75 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -317,6 +317,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: _td('Show line numbers in code blocks'),
         default: true,
     },
+    "scrollToBottomOnMessageSent": {
+        supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+        displayName: _td('Jump to the bottom of the timeline when you send a message'),
+        default: true,
+    },
     "Pill.shouldShowPillAvatar": {
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
         displayName: _td('Show avatars in user and room mentions'),

From 1387c9f94db2815184f920c516a40b2809916fcb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 18:35:19 +0100
Subject: [PATCH 186/389] Display scrollToBottomOnMessageSent
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 .../views/settings/tabs/user/PreferencesUserSettingsTab.js       | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index 04fcea39dc..5e1c2e7288 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -48,6 +48,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'showRedactions',
         'enableSyntaxHighlightLanguageDetection',
         'expandCodeByDefault',
+        `scrollToBottomOnMessageSent`,
         'showCodeLineNumbers',
         'showJoinLeaves',
         'showAvatarChanges',

From 361420bf6826dc309f6f46c4f9b4d0bed4790982 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 18:35:45 +0100
Subject: [PATCH 187/389] Use scrollToBottomOnMessageSent
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/SendMessageComposer.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 62c474e417..c2baa2762c 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -403,7 +403,9 @@ export default class SendMessageComposer extends React.Component {
         this._editorRef.clearUndoHistory();
         this._editorRef.focus();
         this._clearStoredEditorState();
-        dis.dispatch({action: "scroll_to_bottom"});
+        if (SettingsStore.getValue("scrollToBottomOnMessageSent")) {
+            dis.dispatch({action: "scroll_to_bottom"});
+        }
     }
 
     componentWillUnmount() {

From 83df645dbbadcab2a003e14f946f11cf4daa59e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 26 Feb 2021 18:35:53 +0100
Subject: [PATCH 188/389] i18n
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/i18n/strings/en_EN.json | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5bbbdf60b5..4bd7131bfe 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -808,6 +808,7 @@
     "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
     "Expand code blocks by default": "Expand code blocks by default",
     "Show line numbers in code blocks": "Show line numbers in code blocks",
+    "Jump to the bottom of the timeline when you send a message": "Jump to the bottom of the timeline when you send a message",
     "Show avatars in user and room mentions": "Show avatars in user and room mentions",
     "Enable big emoji in chat": "Enable big emoji in chat",
     "Send typing notifications": "Send typing notifications",

From 414f18b19f1c33d4cfaff8fa0440130b76d86dcf Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Fri, 26 Feb 2021 18:01:24 +0000
Subject: [PATCH 189/389] Translated using Weblate (German)

Currently translated at 99.2% (2759 of 2780 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 | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 3f657e105b..269fef0da1 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -16,7 +16,7 @@
     "Deops user with given id": "Setzt das Berechtigungslevel des/der Benutzer:in mit der angegebenen ID zurück",
     "Invites user with given id to current room": "Lädt den/die Benutzer:in mit der angegebenen ID in den aktuellen Raum ein",
     "Kicks user with given id": "Benutzer:in mit der angegebenen ID kicken",
-    "Changes your display nickname": "Ändert deinen angezeigten Nicknamen",
+    "Changes your display nickname": "Ändert deinen Anzeigenamen",
     "Change Password": "Passwort ändern",
     "Searches DuckDuckGo for results": "Verwendet DuckDuckGo für Suchergebnisse",
     "Commands": "Kommandos",
@@ -115,7 +115,7 @@
     "You are already in a call.": "Du bist bereits in einem Gespräch.",
     "You cannot place a call with yourself.": "Du kannst keinen Anruf mit dir selbst starten.",
     "You cannot place VoIP calls in this browser.": "VoIP-Gespräche werden von diesem Browser nicht unterstützt.",
-    "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Home-Server verbunden zu sein.",
+    "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Homeserver verbunden zu sein.",
     "Sun": "So",
     "Mon": "Mo",
     "Tue": "Di",
@@ -636,7 +636,7 @@
     "Which officially provided instance you are using, if any": "Welche offiziell angebotene Instanz du nutzt, wenn überhaupt eine",
     "<a>In reply to</a> <pill>": "<a>Als Antwort auf</a> <pill>",
     "This room is not public. You will not be able to rejoin without an invite.": "Dies ist kein öffentlicher Raum. Du wirst diesen nicht ohne Einladung wieder beitreten können.",
-    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s änderte den Anzeigenamen auf %(displayName)s.",
+    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s hat den Anzeigenamen zu %(displayName)s geändert.",
     "Failed to set direct chat tag": "Fehler beim Setzen der Direkt-Chat-Markierung",
     "Failed to remove tag %(tagName)s from room": "Entfernen der Raum-Kennzeichnung %(tagName)s fehlgeschlagen",
     "Failed to add tag %(tagName)s to room": "Fehler beim Hinzufügen des \"%(tagName)s\"-Tags an dem Raum",
@@ -834,7 +834,7 @@
     "This event could not be displayed": "Dieses Ereignis konnte nicht angezeigt werden",
     "A call is currently being placed!": "Ein Anruf wurde schon gestartet!",
     "Permission Required": "Berechtigung benötigt",
-    "You do not have permission to start a conference call in this room": "Du hast keine Berechtigung um ein Konferenzgespräch in diesem Raum zu starten",
+    "You do not have permission to start a conference call in this room": "Du hast keine Berechtigung ein Konferenzgespräch in diesem Raum zu starten",
     "Failed to remove widget": "Widget konnte nicht entfernt werden",
     "An error ocurred whilst trying to remove the widget from the room": "Ein Fehler trat auf während versucht wurde, das Widget aus diesem Raum zu entfernen",
     "System Alerts": "System-Benachrichtigung",
@@ -2944,8 +2944,8 @@
     "Homeserver": "Heimserver",
     "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.": "Du kannst in den benutzerdefinierten Serveroptionen eine andere Heimserver-URL angeben, um dich bei anderen Matrixservern anzumelden.",
     "Server Options": "Servereinstellungen",
-    "No other application is using the webcam": "Keine andere Anwendung auf die Webcam zugreift",
-    "Permission is granted to use the webcam": "Auf die Webcam zugegriffen werden darf",
+    "No other application is using the webcam": "keine andere Anwendung auf die Webcam zugreift",
+    "Permission is granted to use the webcam": "auf die Webcam zugegriffen werden darf",
     "A microphone and webcam are plugged in and set up correctly": "Mikrofon und Webcam eingesteckt und richtig eingerichtet sind",
     "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon zugegriffen werden konnte. Stelle sicher, dass das Mikrofon richtig eingesteckt und eingerichtet ist.",
     "Call failed because no webcam or microphone could not be accessed. Check that:": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon oder die Webcam zugegriffen werden konnte. Stelle sicher, dass:",

From d83b935fc0f80bf9460b94254a54eb2cf0e52a63 Mon Sep 17 00:00:00 2001
From: libexus <Asterixeins324@gmail.com>
Date: Thu, 25 Feb 2021 07:30:54 +0000
Subject: [PATCH 190/389] Translated using Weblate (German)

Currently translated at 99.2% (2759 of 2780 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 | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 269fef0da1..ff754e7d7e 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -181,7 +181,7 @@
     "%(senderName)s unbanned %(targetName)s.": "%(senderName)s hat die Verbannung von %(targetName)s aufgehoben.",
     "Usage": "Verwendung",
     "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s hat die Einladung für %(targetName)s zurückgezogen.",
-    "You need to be able to invite users to do that.": "Du brauchst die Berechtigung Benutzer:innen einzuladen haben, um diese Aktion ausführen zu können.",
+    "You need to be able to invite users to do that.": "Du musst die Berechtigung \"Benutzer:innen einladen\" haben, um diese Aktion ausführen zu können.",
     "You need to be logged in.": "Du musst angemeldet sein.",
     "There are no visible files in this room": "Es gibt keine sichtbaren Dateien in diesem Raum",
     "Connectivity to the server has been lost.": "Verbindung zum Server wurde unterbrochen.",
@@ -1636,7 +1636,7 @@
     "Use Single Sign On to continue": "Single-Sign-On zum Fortfahren nutzen",
     "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte E-Mail-Adresse mit Single Sign-On, um deine Identität nachzuweisen.",
     "Single Sign On": "Single Sign-On",
-    "Confirm adding email": "Bestätige hinzugefügte E-Mail-Addresse",
+    "Confirm adding email": "Hinzugefügte E-Mail-Addresse bestätigen",
     "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels Single Sign-On nachweist.",
     "Click the button below to confirm adding this phone number.": "Klicke unten die Schaltfläche, um die hinzugefügte Telefonnummer zu bestätigen.",
     "If you cancel now, you won't complete your operation.": "Wenn du jetzt abbrichst, wirst du deinen Vorgang nicht fertigstellen.",
@@ -3059,5 +3059,16 @@
     "Cookie Policy": "Cookie-Richtlinie",
     "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Erfahren mehr in unserer <privacyPolicyLink />, <termsOfServiceLink /> und <cookiePolicyLink />.",
     "Failed to connect to your homeserver. Please close this dialog and try again.": "Verbindung zum Homeserver fehlgeschlagen. Bitte schließe diesen Dialog and versuche es erneut.",
-    "Abort": "Abbrechen"
+    "Abort": "Abbrechen",
+    "Upgrade to %(hostSignupBrand)s": "Zu %(hostSignupBrand)s upgraden",
+    "Edit Values": "Werte bearbeiten",
+    "Value in this room:": "Wert in diesem Raum:",
+    "Value:": "Wert:",
+    "Level": "Level",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Diese Benutzeroberfläche prüft nicht auf richtige Datentypen. Benutzung auf eigene Gefahr.",
+    "Setting:": "Einstellung:",
+    "Value": "Wert",
+    "Setting ID": "Einstellungs-ID",
+    "Failed to save settings": "Einstellungen konnten nicht gespeichert werden",
+    "Show chat effects (animations when receiving e.g. confetti)": "Animierte Chateffekte zeigen, wenn z.B. Konfetti-Emojis erhalten werden"
 }

From de73cef7e61ea57e1750f246e0a9f7d9ef785472 Mon Sep 17 00:00:00 2001
From: libexus <Asterixeins324@gmail.com>
Date: Fri, 26 Feb 2021 18:02:04 +0000
Subject: [PATCH 191/389] Translated using Weblate (German)

Currently translated at 99.2% (2759 of 2780 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 ff754e7d7e..29b86ac641 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -636,7 +636,7 @@
     "Which officially provided instance you are using, if any": "Welche offiziell angebotene Instanz du nutzt, wenn überhaupt eine",
     "<a>In reply to</a> <pill>": "<a>Als Antwort auf</a> <pill>",
     "This room is not public. You will not be able to rejoin without an invite.": "Dies ist kein öffentlicher Raum. Du wirst diesen nicht ohne Einladung wieder beitreten können.",
-    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s hat den Anzeigenamen zu %(displayName)s geändert.",
+    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s hat den Anzeigenamen auf %(displayName)s geändert.",
     "Failed to set direct chat tag": "Fehler beim Setzen der Direkt-Chat-Markierung",
     "Failed to remove tag %(tagName)s from room": "Entfernen der Raum-Kennzeichnung %(tagName)s fehlgeschlagen",
     "Failed to add tag %(tagName)s to room": "Fehler beim Hinzufügen des \"%(tagName)s\"-Tags an dem Raum",

From 4e27b00cf370e2e4bbf42da9aee48823dd1c4daa Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 26 Feb 2021 13:46:39 -0700
Subject: [PATCH 192/389] Move call buttons to the room header

This is to make some room in the composer for voice messages. The hangup behaviour is intentionally lost by this change as the VOIP UX is intended to rely on dedicated hangup buttons instead.
---
 res/css/views/rooms/_MessageComposer.scss     |  12 --
 res/css/views/rooms/_RoomHeader.scss          |  13 ++
 src/components/structures/RoomView.tsx        |  11 +-
 src/components/views/rooms/MessageComposer.js | 121 ------------------
 src/components/views/rooms/RoomHeader.js      |  20 +++
 src/settings/Settings.ts                      |   2 +
 6 files changed, 45 insertions(+), 134 deletions(-)

diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index 71c0db947e..69e8b50501 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -227,18 +227,6 @@ limitations under the License.
     mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
 }
 
-.mx_MessageComposer_hangup::before {
-    mask-image: url('$(res)/img/element-icons/call/hangup.svg');
-}
-
-.mx_MessageComposer_voicecall::before {
-    mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
-}
-
-.mx_MessageComposer_videocall::before {
-    mask-image: url('$(res)/img/element-icons/call/video-call.svg');
-}
-
 .mx_MessageComposer_emoji::before {
     mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
 }
diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss
index a23a44906f..387d1588a3 100644
--- a/res/css/views/rooms/_RoomHeader.scss
+++ b/res/css/views/rooms/_RoomHeader.scss
@@ -252,6 +252,19 @@ limitations under the License.
     mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
 }
 
+.mx_RoomHeader_voiceCallButton::before {
+    mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
+
+    // The call button SVG is padded slightly differently, so match it up to the size
+    // of the other icons
+    mask-size: 20px;
+    mask-position: center;
+}
+
+.mx_RoomHeader_videoCallButton::before {
+    mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+}
+
 .mx_RoomHeader_showPanel {
     height: 16px;
 }
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 68ab3c6e0c..6c8560f42c 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -34,7 +34,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
 import ContentMessages from '../../ContentMessages';
 import Modal from '../../Modal';
 import * as sdk from '../../index';
-import CallHandler from '../../CallHandler';
+import CallHandler, { PlaceCallType } from '../../CallHandler';
 import dis from '../../dispatcher/dispatcher';
 import Tinter from '../../Tinter';
 import rateLimitedFunc from '../../ratelimitedfunc';
@@ -1352,6 +1352,14 @@ export default class RoomView extends React.Component<IProps, IState> {
         SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
     };
 
+    private onCallPlaced = (type: PlaceCallType) => {
+        dis.dispatch({
+            action: 'place_call',
+            type: type,
+            room_id: this.state.room.roomId,
+        });
+    };
+
     private onSettingsClick = () => {
         dis.dispatch({ action: "open_room_settings" });
     };
@@ -2031,6 +2039,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                             e2eStatus={this.state.e2eStatus}
                             onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
                             appsShown={this.state.showApps}
+                            onCallPlaced={this.onCallPlaced}
                         />
                         <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
                             <div className="mx_RoomView_body">
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index 6a867386f7..c38d40020f 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -50,97 +50,6 @@ ComposerAvatar.propTypes = {
     me: PropTypes.object.isRequired,
 };
 
-function CallButton(props) {
-    const onVoiceCallClick = (ev) => {
-        dis.dispatch({
-            action: 'place_call',
-            type: PlaceCallType.Voice,
-            room_id: props.roomId,
-        });
-    };
-
-    return (<AccessibleTooltipButton
-        className="mx_MessageComposer_button mx_MessageComposer_voicecall"
-        onClick={onVoiceCallClick}
-        title={_t('Voice call')}
-    />);
-}
-
-CallButton.propTypes = {
-    roomId: PropTypes.string.isRequired,
-};
-
-function VideoCallButton(props) {
-    const onCallClick = (ev) => {
-        dis.dispatch({
-            action: 'place_call',
-            type: ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video,
-            room_id: props.roomId,
-        });
-    };
-
-    return <AccessibleTooltipButton
-        className="mx_MessageComposer_button mx_MessageComposer_videocall"
-        onClick={onCallClick}
-        title={_t('Video call')}
-    />;
-}
-
-VideoCallButton.propTypes = {
-    roomId: PropTypes.string.isRequired,
-};
-
-function HangupButton(props) {
-    const onHangupClick = () => {
-        if (props.isConference) {
-            dis.dispatch({
-                action: props.canEndConference ? 'end_conference' : 'hangup_conference',
-                room_id: props.roomId,
-            });
-            return;
-        }
-
-        const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
-        if (!call) {
-            return;
-        }
-
-        const action = call.state === CallState.Ringing ? 'reject' : 'hangup';
-
-        dis.dispatch({
-            action,
-            // hangup the call for this room. NB. We use the room in props as the room ID
-            // as call.roomId may be the 'virtual room', and the dispatch actions always
-            // use the user-facing room (there was a time when we deliberately used
-            // call.roomId and *not* props.roomId, but that was for the old
-            // style Freeswitch conference calls and those times are gone.)
-            room_id: props.roomId,
-        });
-    };
-
-    let tooltip = _t("Hangup");
-    if (props.isConference && props.canEndConference) {
-        tooltip = _t("End conference");
-    }
-
-    const canLeaveConference = !props.isConference ? true : props.isInConference;
-    return (
-        <AccessibleTooltipButton
-            className="mx_MessageComposer_button mx_MessageComposer_hangup"
-            onClick={onHangupClick}
-            title={tooltip}
-            disabled={!canLeaveConference}
-        />
-    );
-}
-
-HangupButton.propTypes = {
-    roomId: PropTypes.string.isRequired,
-    isConference: PropTypes.bool.isRequired,
-    canEndConference: PropTypes.bool,
-    isInConference: PropTypes.bool,
-};
-
 const EmojiButton = ({addEmoji}) => {
     const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
 
@@ -265,7 +174,6 @@ export default class MessageComposer extends React.Component {
         this.state = {
             tombstone: this._getRoomTombstone(),
             canSendMessages: this.props.room.maySendMessage(),
-            showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
             hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
             joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
         };
@@ -405,12 +313,7 @@ export default class MessageComposer extends React.Component {
         ];
 
         if (!this.state.tombstone && this.state.canSendMessages) {
-            // This also currently includes the call buttons. Really we should
-            // check separately for whether we can call, but this is slightly
-            // complex because of conference calls.
-
             const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer");
-            const callInProgress = this.props.callState && this.props.callState !== 'ended';
 
             controls.push(
                 <SendMessageComposer
@@ -430,30 +333,6 @@ export default class MessageComposer extends React.Component {
                 SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
                 controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
             }
-
-            if (this.state.showCallButtons) {
-                if (this.state.hasConference) {
-                    const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
-                    controls.push(
-                        <HangupButton
-                            key="controls_hangup"
-                            roomId={this.props.room.roomId}
-                            isConference={true}
-                            canEndConference={canEndConf}
-                            isInConference={this.state.joinedConference}
-                        />,
-                    );
-                } else if (callInProgress) {
-                    controls.push(
-                        <HangupButton key="controls_hangup" roomId={this.props.room.roomId} isConference={false} />,
-                    );
-                } else {
-                    controls.push(
-                        <CallButton key="controls_call" roomId={this.props.room.roomId} />,
-                        <VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
-                    );
-                }
-            }
         } else if (this.state.tombstone) {
             const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
 
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 93055c69f5..6736600bc8 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -31,6 +31,7 @@ import {DefaultTagID} from "../../../stores/room-list/models";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import RoomTopic from "../elements/RoomTopic";
 import RoomName from "../elements/RoomName";
+import {PlaceCallType} from "../../../CallHandler";
 
 export default class RoomHeader extends React.Component {
     static propTypes = {
@@ -45,6 +46,7 @@ export default class RoomHeader extends React.Component {
         e2eStatus: PropTypes.string,
         onAppsClick: PropTypes.func,
         appsShown: PropTypes.bool,
+        onCallPlaced: PropTypes.func, // (PlaceCallType) => void;
     };
 
     static defaultProps = {
@@ -226,8 +228,26 @@ export default class RoomHeader extends React.Component {
                     title={_t("Search")} />;
         }
 
+        let voiceCallButton;
+        let videoCallButton;
+        if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) {
+            voiceCallButton =
+                <AccessibleTooltipButton
+                    className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
+                    onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
+                    title={_t("Voice call")} />;
+            videoCallButton =
+                <AccessibleTooltipButton
+                    className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
+                    onClick={(ev) => this.props.onCallPlaced(
+                        ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)}
+                    title={_t("Video call")} />;
+        }
+
         const rightRow =
             <div className="mx_RoomHeader_buttons">
+                { videoCallButton }
+                { voiceCallButton }
                 { pinnedEventsButton }
                 { forgetButton }
                 { appsButton }
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index b452f10e73..43210021e5 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -632,6 +632,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         default: 3000,
     },
     "showCallButtonsInComposer": {
+        // Dev note: This is no longer "in composer" but is instead "in room header".
+        // TODO: Rename with settings v3
         supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
         default: true,
         controller: new UIFeatureController(UIFeature.Voip),

From 855ee068c3c26d37488cf1fe47e6ae62bdb74bc9 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 26 Feb 2021 13:50:03 -0700
Subject: [PATCH 193/389] Appease the linter

---
 src/components/views/rooms/MessageComposer.js | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index c38d40020f..eea6a6b802 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -1,7 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017, 2018 New Vector Ltd
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2015-2018, 2020, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,7 +17,6 @@ import React, {createRef} from 'react';
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
-import CallHandler from '../../../CallHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
@@ -33,11 +30,8 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import ReplyPreview from "./ReplyPreview";
 import {UIFeature} from "../../../settings/UIFeature";
 import WidgetStore from "../../../stores/WidgetStore";
-import WidgetUtils from "../../../utils/WidgetUtils";
 import {UPDATE_EVENT} from "../../../stores/AsyncStore";
 import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
-import { PlaceCallType } from "../../../CallHandler";
-import { CallState } from 'matrix-js-sdk/src/webrtc/call';
 
 function ComposerAvatar(props) {
     const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');

From 01da8633d48a2c04029d5b4bbf80d9cf2a86e63f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 26 Feb 2021 13:50:22 -0700
Subject: [PATCH 194/389] i18n

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

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f443c4961b..0be7e6e02b 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1416,9 +1416,6 @@
     "Invited": "Invited",
     "Filter room members": "Filter room members",
     "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
-    "Voice call": "Voice call",
-    "Video call": "Video call",
-    "Hangup": "Hangup",
     "Emoji picker": "Emoji picker",
     "Upload file": "Upload file",
     "Send an encrypted reply…": "Send an encrypted reply…",
@@ -1476,6 +1473,8 @@
     "Hide Widgets": "Hide Widgets",
     "Show Widgets": "Show Widgets",
     "Search": "Search",
+    "Voice call": "Voice call",
+    "Video call": "Video call",
     "Start a Conversation": "Start a Conversation",
     "Open dial pad": "Open dial pad",
     "Invites": "Invites",

From bfe3d648e294f1518ece26a3b2c83b3a3d29f330 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 26 Feb 2021 22:08:36 +0000
Subject: [PATCH 195/389] Update src/stores/room-list/algorithms/Algorithm.ts

Co-authored-by: Travis Ralston <travpc@gmail.com>
---
 src/stores/room-list/algorithms/Algorithm.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts
index 40fdae5ae4..fed3099325 100644
--- a/src/stores/room-list/algorithms/Algorithm.ts
+++ b/src/stores/room-list/algorithms/Algorithm.ts
@@ -186,7 +186,7 @@ export class Algorithm extends EventEmitter {
     }
 
     private async doUpdateStickyRoom(val: Room) {
-        // no-op sticky rooms
+        // no-op sticky rooms for spaces - they're effectively virtual rooms
         if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") val = null;
 
         // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,

From e3065f5a02533c38f0a7b770c519c5314d27880d Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Fri, 26 Feb 2021 17:09:54 -0500
Subject: [PATCH 196/389] Support sending invite reasons with MultiInviter

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/utils/MultiInviter.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js
index 7d1c900360..63d3942b37 100644
--- a/src/utils/MultiInviter.js
+++ b/src/utils/MultiInviter.js
@@ -53,13 +53,15 @@ export default class MultiInviter {
      * instance of the class.
      *
      * @param {array} addrs Array of addresses to invite
+     * @param {string} reason Reason for inviting (optional)
      * @returns {Promise} Resolved when all invitations in the queue are complete
      */
-    invite(addrs) {
+    invite(addrs, reason) {
         if (this.addrs.length > 0) {
             throw new Error("Already inviting/invited");
         }
         this.addrs.push(...addrs);
+        this.reason = reason;
 
         for (const addr of this.addrs) {
             if (getAddressType(addr) === null) {
@@ -123,7 +125,7 @@ export default class MultiInviter {
                 }
             }
 
-            return MatrixClientPeg.get().invite(roomId, addr);
+            return MatrixClientPeg.get().invite(roomId, addr, undefined, this.reason);
         } else {
             throw new Error('Unsupported address');
         }

From c25a8b70fa051afe102a301d146448f44cd51c3d Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Fri, 26 Feb 2021 17:10:20 -0500
Subject: [PATCH 197/389] Support sending invite reasons with /invite command

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/SlashCommands.tsx | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 6b5f261374..aedcf7af8c 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -441,15 +441,14 @@ export const Commands = [
     }),
     new Command({
         command: 'invite',
-        args: '<user-id>',
+        args: '<user-id> [<reason>]',
         description: _td('Invites user with given id to current room'),
         runFn: function(roomId, args) {
             if (args) {
-                const matches = args.match(/^(\S+)$/);
-                if (matches) {
+                const [address, reason] = args.split(/\s+(.+)/);
+                if (address) {
                     // We use a MultiInviter to re-use the invite logic, even though
                     // we're only inviting one user.
-                    const address = matches[1];
                     // If we need an identity server but don't have one, things
                     // get a bit more complex here, but we try to show something
                     // meaningful.
@@ -490,7 +489,7 @@ export const Commands = [
                     }
                     const inviter = new MultiInviter(roomId);
                     return success(prom.then(() => {
-                        return inviter.invite([address]);
+                        return inviter.invite([address], reason);
                     }).then(() => {
                         if (inviter.getCompletionState(address) !== "invited") {
                             throw new Error(inviter.getErrorText(address));

From 208faf6d46e30a77db7f4dd4b6d6a2bd2dba138f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 26 Feb 2021 22:21:14 -0700
Subject: [PATCH 198/389] Update velocity-animate to the latest beta

This is the primary change in this PR: the new beta (which has been untouched for a year as of writing) actually does a better job of handling concurrent read receipts, this patching holes.

The beta doesn't have the same leak as v1, so we can remove the metadata hack from our side (it doesn't use jQuery's data anymore).

Note that this change on its own introduces an annoying bug where every second update to a read receipt will throw it 14px to the right - more on that in the next commit.
---
 package.json        |  2 +-
 src/Velociraptor.js | 23 ++++-------------------
 yarn.lock           |  8 ++++----
 3 files changed, 9 insertions(+), 24 deletions(-)

diff --git a/package.json b/package.json
index d4f931d811..10480b8af9 100644
--- a/package.json
+++ b/package.json
@@ -101,7 +101,7 @@
     "tar-js": "^0.3.0",
     "text-encoding-utf-8": "^1.0.2",
     "url": "^0.11.0",
-    "velocity-animate": "^1.5.2",
+    "velocity-animate": "^2.0.6",
     "what-input": "^5.2.10",
     "zxcvbn": "^4.4.2"
   },
diff --git a/src/Velociraptor.js b/src/Velociraptor.js
index ce52f60dbd..2da54babe5 100644
--- a/src/Velociraptor.js
+++ b/src/Velociraptor.js
@@ -118,25 +118,10 @@ export default class Velociraptor extends React.Component {
                     domNode.style.visibility = restingStyle.visibility;
                 });
 
-            /*
-            console.log("enter:",
-                        JSON.stringify(transitionOpts[i-1]),
-                        "->",
-                        JSON.stringify(restingStyle));
-            */
-        } else if (node === null) {
-            // Velocity stores data on elements using the jQuery .data()
-            // method, and assumes you'll be using jQuery's .remove() to
-            // remove the element, but we don't use jQuery, so we need to
-            // blow away the element's data explicitly otherwise it will leak.
-            // This uses Velocity's internal jQuery compatible wrapper.
-            // See the bug at
-            // https://github.com/julianshapiro/velocity/issues/300
-            // and the FAQ entry, "Preventing memory leaks when
-            // creating/destroying large numbers of elements"
-            // (https://github.com/julianshapiro/velocity/issues/47)
-            const domNode = ReactDom.findDOMNode(this.nodes[k]);
-            if (domNode) Velocity.Utilities.removeData(domNode);
+            // console.log("enter:",
+            //             JSON.stringify(transitionOpts[i-1]),
+            //             "->",
+            //             JSON.stringify(restingStyle));
         }
         this.nodes[k] = node;
     }
diff --git a/yarn.lock b/yarn.lock
index 01450908cc..5939a89f58 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8114,10 +8114,10 @@ validate-npm-package-license@^3.0.1:
     spdx-correct "^3.0.0"
     spdx-expression-parse "^3.0.0"
 
-velocity-animate@^1.5.2:
-  version "1.5.2"
-  resolved "https://registry.yarnpkg.com/velocity-animate/-/velocity-animate-1.5.2.tgz#5a351d75fca2a92756f5c3867548b873f6c32105"
-  integrity sha512-m6EXlCAMetKztO1ppBhGU1/1MR3IiEevO6ESq6rcrSQ3Q77xYSW13jkfXW88o4xMrkXJhy/U7j4wFR/twMB0Eg==
+velocity-animate@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/velocity-animate/-/velocity-animate-2.0.6.tgz#1811ca14df7fbbef05740256f6cec0fd1b76575f"
+  integrity sha512-tU+/UtSo3GkIjEfk2KM4e24DvpgX0+FzfLr7XqNwm9BCvZUtbCHPq/AFutx/Mkp2bXlUS9EcX8yxu8XmzAv2Kw==
 
 verror@1.10.0:
   version "1.10.0"

From b3142d613806ed243947ddbf304a33f6e97219b5 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 26 Feb 2021 22:24:36 -0700
Subject: [PATCH 199/389] Offset read receipt start positions by 1px

As mentioned in 208faf6d46e30a77db7f4dd4b6d6a2bd2dba138f, the velocity-animate update causes read receipts to occasionally show up 14px to the right of where they should be. This is because the read receipt width is 14px, and velocity-animate will *not* translate `left` if it isn't changing. Unfortunately, it's smart enough to realize that `-0px` is `0px`, so we end up having to specify `1px`.

The comment already mentions it, but this should have no perceived effect for the user. During development I could not tell if the 1px was being applied during the animation, implying that it's a meaningless value. It's a bit unfortunate for those who know that it's translating left by 1px, but hopefully they'll be able to unsee that in time.
---
 src/components/views/rooms/ReadReceiptMarker.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js
index c19247ef5a..f5c676b841 100644
--- a/src/components/views/rooms/ReadReceiptMarker.js
+++ b/src/components/views/rooms/ReadReceiptMarker.js
@@ -155,7 +155,13 @@ export default class ReadReceiptMarker extends React.Component {
 
         // then shift to the rightmost column,
         // and then it will drop down to its resting position
-        startStyles.push({ top: startTopOffset+'px', left: '0px' });
+        //
+        // XXX: We use a `left: 1px` to trick velocity-animate into actually animating. This
+        // is a very annoying bug where if it thinks there's no change to `left` then it'll
+        // skip applying it, thus making our read receipt at +14px instead of +0px like it
+        // should be. This does cause 1px of drift for read receipts, however nobody should
+        // notice this while it's also falling.
+        startStyles.push({ top: startTopOffset+'px', left: '1px' });
         enterTransitionOpts.push({
             duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
             easing: bounce ? 'easeOutBounce' : 'easeOutCubic',

From 76ad93b9370e2e6922b54f2268a0d4f9ebe3dc9e Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 26 Feb 2021 22:25:50 -0700
Subject: [PATCH 200/389] Put speed holes in the code

We can make read receipts more efficient (and avoid double-animation) by using `PureComponent` which no-ops useless updates for us.
---
 src/components/views/rooms/ReadReceiptMarker.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js
index f5c676b841..8b2f5d27b5 100644
--- a/src/components/views/rooms/ReadReceiptMarker.js
+++ b/src/components/views/rooms/ReadReceiptMarker.js
@@ -32,7 +32,7 @@ try {
 } catch (e) {
 }
 
-export default class ReadReceiptMarker extends React.Component {
+export default class ReadReceiptMarker extends React.PureComponent {
     static propTypes = {
         // the RoomMember to show the RR for
         member: PropTypes.object,

From 0dd4d45c49eeb985c7febda543145021c49aa92f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 26 Feb 2021 22:36:42 -0700
Subject: [PATCH 201/389] Disable velocity mock option

This appears to have been removed in the beta
---
 test/components/structures/MessagePanel-test.js | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index f40f8c5187..2fd5bd6ad1 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -35,7 +35,6 @@ const mockclock = require('../../mock-clock');
 import Adapter from "enzyme-adapter-react-16";
 import { configure, mount } from "enzyme";
 
-import Velocity from 'velocity-animate';
 import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
 import RoomContext from "../../../src/contexts/RoomContext";
 import DMRoomMap from "../../../src/utils/DMRoomMap";
@@ -75,18 +74,10 @@ describe('MessagePanel', function() {
             return arg === "showDisplaynameChanges";
         });
 
-        // This option clobbers the duration of all animations to be 1ms
-        // which makes unit testing a lot simpler (the animation doesn't
-        // complete without this even if we mock the clock and tick it
-        // what should be the correct amount of time).
-        Velocity.mock = true;
-
         DMRoomMap.makeShared();
     });
 
     afterEach(function() {
-        delete Velocity.mock;
-
         clock.uninstall();
     });
 

From 905f5300f49e2770d77d8d3e9c86484735bcbdaa Mon Sep 17 00:00:00 2001
From: Nikita Epifanov <NikGreens@protonmail.com>
Date: Thu, 25 Feb 2021 11:01:31 +0000
Subject: [PATCH 202/389] Translated using Weblate (Russian)

Currently translated at 99.4% (2765 of 2780 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 | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index a72eef2c58..65aeaf82d9 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -3057,5 +3057,20 @@
     "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Ваш домашний сервер был недоступен, и мы не смогли войти в систему. Пожалуйста, попробуйте снова через пару минут. Если ситуация по-прежнему не меняется, обратитесь к администратору домашнего сервера за дополнительной информацией.",
     "Try again": "Попробовать ещё раз",
     "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Мы попросили браузер запомнить, какой домашний сервер вы используете для входа в систему, но, к сожалению, ваш браузер забыл об этом. Перейдите на страницу входа и попробуйте ещё раз.",
-    "We couldn't log you in": "Нам не удалось войти в систему"
+    "We couldn't log you in": "Нам не удалось войти в систему",
+    "Upgrade to %(hostSignupBrand)s": "Перейти на %(hostSignupBrand)s",
+    "Edit Values": "Изменить значения",
+    "Value in this room:": "Значение в этой комнате:",
+    "Value:": "Значение:",
+    "Setting:": "Настройки:",
+    "Setting ID": "ID настроек",
+    "Save setting values": "Сохранить значения настроек",
+    "Settable at room": "Устанавливается для комнаты",
+    "Settable at global": "Устанавливается на глобальном уровне",
+    "Level": "Уровень",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Этот пользовательский интерфейс НЕ проверяет типы значений. Используйте на свой риск.",
+    "Value in this room": "Значение в этой комнате",
+    "Value": "Значение",
+    "Failed to save settings": "Не удалось сохранить настройки",
+    "Show chat effects (animations when receiving e.g. confetti)": "Показать эффекты чата (анимация при получении, например, конфетти)"
 }

From 4b771b0634594dcc8205fcc19ac49e107079c24f Mon Sep 17 00:00:00 2001
From: libexus <Asterixeins324@gmail.com>
Date: Sat, 27 Feb 2021 14:58:24 +0000
Subject: [PATCH 203/389] Translated using Weblate (German)

Currently translated at 99.2% (2759 of 2780 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 | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 29b86ac641..e2e2e0718a 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1569,7 +1569,7 @@
     "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s hat die alternative Adresse %(addresses)s für diesen Raum entfernt.",
     "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s hat die alternative Adresse für diesen Raum geändert.",
     "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s hat die Haupt- und Alternativadressen für diesen Raum geändert.",
-    "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Nutzer!nnen, die %(glob)s entsprechen",
+    "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Benutzer:innen, die %(glob)s entsprechen",
     "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Räume, die %(glob)s entsprechen",
     "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel für Server, die %(glob)s entsprechen",
     "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s entfernte die Ausschluss-Regel, die %(glob)s entspricht",
@@ -2567,7 +2567,7 @@
     "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",
+    "Change the avatar of this room": "Icon 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",
@@ -3036,7 +3036,7 @@
     "Converts the room to a DM": "Wandelt den Raum zu Direktnachricht um",
     "Something went wrong in confirming your identity. Cancel and try again.": "Bei der Bestätigung deiner Identität ist ein Fehler aufgetreten. Abbrechen und erneut versuchen.",
     "Use app": "App verwenden",
-    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web ist experimentell auf mobilen Endgeräten. Für eine bessere Erfahrung und die neuesten Erweiterungen, nutze unsere freie, native App.",
+    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web ist auf mobilen Endgeräten experimentell. Für eine bessere Erfahrung und die neuesten Features, nutze unsere freie, native App.",
     "Use app for a better experience": "Nutze die App für eine bessere Erfahrung",
     "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Wir haben deinen Browser gebeten, sich zu merken, bei welchem Homeserver du dich anmeldest, aber dein Browser hat dies leider vergessen. Gehe zur Anmeldeseite und versuche es erneut.",
     "Show stickers button": "Sticker-Schaltfläche anzeigen",

From 844ed6b0b9c5e8a99aad99192e9506b047d72904 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Fri, 26 Feb 2021 18:04:45 +0000
Subject: [PATCH 204/389] Translated using Weblate (German)

Currently translated at 99.2% (2759 of 2780 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, 2 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index e2e2e0718a..b615cda081 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -636,7 +636,7 @@
     "Which officially provided instance you are using, if any": "Welche offiziell angebotene Instanz du nutzt, wenn überhaupt eine",
     "<a>In reply to</a> <pill>": "<a>Als Antwort auf</a> <pill>",
     "This room is not public. You will not be able to rejoin without an invite.": "Dies ist kein öffentlicher Raum. Du wirst diesen nicht ohne Einladung wieder beitreten können.",
-    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s hat den Anzeigenamen auf %(displayName)s geändert.",
+    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s hat den Anzeigenamen zu %(displayName)s geändert.",
     "Failed to set direct chat tag": "Fehler beim Setzen der Direkt-Chat-Markierung",
     "Failed to remove tag %(tagName)s from room": "Entfernen der Raum-Kennzeichnung %(tagName)s fehlgeschlagen",
     "Failed to add tag %(tagName)s to room": "Fehler beim Hinzufügen des \"%(tagName)s\"-Tags an dem Raum",
@@ -2664,7 +2664,7 @@
     "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 while running": "Bleib auf deinem 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",

From e43853d6b0aeda0dd0cf342e378977167a9ae614 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Sat, 27 Feb 2021 12:02:24 -0700
Subject: [PATCH 205/389] Use a small fractional value instead

---
 src/components/views/rooms/ReadReceiptMarker.js | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js
index 8b2f5d27b5..ba2b3064fd 100644
--- a/src/components/views/rooms/ReadReceiptMarker.js
+++ b/src/components/views/rooms/ReadReceiptMarker.js
@@ -156,12 +156,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
         // then shift to the rightmost column,
         // and then it will drop down to its resting position
         //
-        // XXX: We use a `left: 1px` to trick velocity-animate into actually animating. This
-        // is a very annoying bug where if it thinks there's no change to `left` then it'll
+        // XXX: We use a fractional left value to trick velocity-animate into actually animating.
+        // This is a very annoying bug where if it thinks there's no change to `left` then it'll
         // skip applying it, thus making our read receipt at +14px instead of +0px like it
-        // should be. This does cause 1px of drift for read receipts, however nobody should
-        // notice this while it's also falling.
-        startStyles.push({ top: startTopOffset+'px', left: '1px' });
+        // should be. This does cause a tiny amount of drift for read receipts, however with a
+        // value so small it's not perceived by a user.
+        // Note: Any smaller values (or trying to interchange units) might cause read receipts to
+        // fail to fall down or cause gaps.
+        startStyles.push({ top: startTopOffset+'px', left: '0.001px' });
         enterTransitionOpts.push({
             duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
             easing: bounce ? 'easeOutBounce' : 'easeOutCubic',

From 188b728f5311e0c607623a58cc336fa434d07b31 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 28 Feb 2021 13:32:17 +0100
Subject: [PATCH 206/389] Fix read receipts for compact layout
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 res/css/views/rooms/_GroupLayout.scss | 13 +++----------
 1 file changed, 3 insertions(+), 10 deletions(-)

diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss
index 543e6ed685..903fabc8fd 100644
--- a/res/css/views/rooms/_GroupLayout.scss
+++ b/res/css/views/rooms/_GroupLayout.scss
@@ -105,16 +105,9 @@ $left-gutter: 64px;
         }
 
         .mx_EventTile_readAvatars {
-            top: 27px;
-        }
-
-        &.mx_EventTile_continuation .mx_EventTile_readAvatars,
-        &.mx_EventTile_emote .mx_EventTile_readAvatars {
-            top: 5px;
-        }
-
-        &.mx_EventTile_info .mx_EventTile_readAvatars {
-            top: 4px;
+            // This aligns the avatar with the last line of the
+            // message. We want to move it one line up - 2rem
+            top: -2rem;
         }
 
         .mx_EventTile_content .markdown-body {

From 94fbd7c9b127faaab1c668a101b6b0506798d767 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Sat, 27 Feb 2021 14:58:41 +0000
Subject: [PATCH 207/389] Translated using Weblate (German)

Currently translated at 99.2% (2759 of 2780 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 b615cda081..8a07b6cd9f 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -3036,7 +3036,7 @@
     "Converts the room to a DM": "Wandelt den Raum zu Direktnachricht um",
     "Something went wrong in confirming your identity. Cancel and try again.": "Bei der Bestätigung deiner Identität ist ein Fehler aufgetreten. Abbrechen und erneut versuchen.",
     "Use app": "App verwenden",
-    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web ist auf mobilen Endgeräten experimentell. Für eine bessere Erfahrung und die neuesten Features, nutze unsere freie, native App.",
+    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web ist auf mobilen Endgeräten experimentell. Für eine bessere Erfahrung und die neuesten Erweiterungen, nutze unsere freie, native App.",
     "Use app for a better experience": "Nutze die App für eine bessere Erfahrung",
     "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Wir haben deinen Browser gebeten, sich zu merken, bei welchem Homeserver du dich anmeldest, aber dein Browser hat dies leider vergessen. Gehe zur Anmeldeseite und versuche es erneut.",
     "Show stickers button": "Sticker-Schaltfläche anzeigen",

From d6d5455a11915a313b137b4a8f98bde762441117 Mon Sep 17 00:00:00 2001
From: Thibault Martin <mail@thibaultmart.in>
Date: Thu, 25 Feb 2021 17:17:39 +0000
Subject: [PATCH 208/389] Translated using Weblate (French)

Currently translated at 99.2% (2758 of 2780 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 | 260 +++++++++++++++++++--------------------
 1 file changed, 130 insertions(+), 130 deletions(-)

diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index c942cae520..304599cd44 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -1,6 +1,6 @@
 {
     "Disinvite": "Désinviter",
-    "Displays action": "Affiche l'action",
+    "Displays action": "Affiche l’action",
     "Download %(text)s": "Télécharger %(text)s",
     "Emoji": "Émojis",
     "%(senderName)s ended the call.": "%(senderName)s a terminé l’appel.",
@@ -32,14 +32,14 @@
     "%(senderName)s banned %(targetName)s.": "%(senderName)s a banni %(targetName)s.",
     "Ban": "Bannir",
     "Banned users": "Utilisateurs bannis",
-    "Bans user with given id": "Bannit l'utilisateur à partir de son identifiant",
+    "Bans user with given id": "Bannit l’utilisateur à partir de son identifiant",
     "Call Timeout": "L’appel a dépassé le délai d'attente maximal",
     "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>.": "Impossible de se connecter au serveur d'accueil en HTTP si l'URL dans la barre de votre explorateur est en HTTPS. Utilisez HTTPS ou <a>activez le support des scripts non-vérifiés</a>.",
     "Change Password": "Changer le mot de passe",
     "%(senderName)s changed their profile picture.": "%(senderName)s a changé son image de profil.",
     "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s a changé le rang de %(powerLevelDiffText)s.",
     "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s a changé le nom du salon en %(roomName)s.",
-    "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s a changé le sujet du salon en \"%(topic)s\".",
+    "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s a changé le sujet du salon en « %(topic)s ».",
     "Changes your display nickname": "Change votre nom affiché",
     "Click here to fix": "Cliquer ici pour réparer",
     "Click to mute audio": "Cliquer pour couper le son",
@@ -54,7 +54,7 @@
     "Create Room": "Créer un salon",
     "Cryptography": "Chiffrement",
     "Current password": "Mot de passe actuel",
-    "/ddg is not a command": "/ddg n'est pas une commande",
+    "/ddg is not a command": "/ddg n’est pas une commande",
     "Deactivate Account": "Fermer le compte",
     "Decrypt %(text)s": "Déchiffrer %(text)s",
     "Deops user with given id": "Retire le rang d’opérateur d’un utilisateur à partir de son identifiant",
@@ -66,7 +66,7 @@
     "Failed to reject invite": "Échec du rejet de l'invitation",
     "Failed to reject invitation": "Échec du rejet de l'invitation",
     "Failed to send email": "Échec de l’envoi de l’e-mail",
-    "Failed to send request.": "Échec de l'envoi de la requête.",
+    "Failed to send request.": "Échec de l’envoi de la requête.",
     "Failed to set display name": "Échec de l'enregistrement du nom affiché",
     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s a accepté l’invitation de %(displayName)s.",
     "Access Token:": "Jeton d’accès :",
@@ -101,17 +101,17 @@
     "%(targetName)s joined the room.": "%(targetName)s a rejoint le salon.",
     "%(senderName)s kicked %(targetName)s.": "%(senderName)s a expulsé %(targetName)s.",
     "Kick": "Expulser",
-    "Kicks user with given id": "Expulse l'utilisateur à partir de son identifiant",
+    "Kicks user with given id": "Expulse l’utilisateur à partir de son identifiant",
     "Labs": "Labo",
     "Leave room": "Quitter le salon",
     "%(targetName)s left the room.": "%(targetName)s a quitté le salon.",
     "Logout": "Se déconnecter",
     "Low priority": "Priorité basse",
-    "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s a rendu l'historique visible à tous les membres du salon, depuis le moment où ils ont été invités.",
-    "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s a rendu l'historique visible à tous les membres du salon, depuis le moment où ils ont rejoint.",
-    "%(senderName)s made future room history visible to all room members.": "%(senderName)s a rendu l'historique visible à tous les membres du salon.",
-    "%(senderName)s made future room history visible to anyone.": "%(senderName)s a rendu l'historique visible à n'importe qui.",
-    "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s a rendu l'historique visible à inconnu (%(visibility)s).",
+    "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s a rendu l’historique visible à tous les membres du salon, depuis le moment où ils ont été invités.",
+    "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s a rendu l’historique visible à tous les membres du salon, à partir de leur arrivée.",
+    "%(senderName)s made future room history visible to all room members.": "%(senderName)s a rendu l’historique visible à tous les membres du salon.",
+    "%(senderName)s made future room history visible to anyone.": "%(senderName)s a rendu l’historique visible à tout le monde.",
+    "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s a rendu l’historique visible à inconnu (%(visibility)s).",
     "Manage Integrations": "Gestion des intégrations",
     "Missing room_id in request": "Absence du room_id dans la requête",
     "Missing user_id in request": "Absence du user_id dans la requête",
@@ -120,7 +120,7 @@
     "New passwords don't match": "Les mots de passe ne correspondent pas",
     "New passwords must match each other.": "Les nouveaux mots de passe doivent être identiques.",
     "not specified": "non spécifié",
-    "(not supported by this browser)": "(non supporté par ce navigateur)",
+    "(not supported by this browser)": "(non pris en charge par ce navigateur)",
     "<not supported>": "<non supporté>",
     "No more results": "Fin des résultats",
     "No results": "Pas de résultat",
@@ -141,7 +141,7 @@
     "No users have specific privileges in this room": "Aucun utilisateur n’a de privilège spécifique dans ce salon",
     "olm version:": "version de olm :",
     "Please check your email and click on the link it contains. Once this is done, click continue.": "Veuillez vérifier vos e-mails et cliquer sur le lien que vous avez reçu. Puis cliquez sur continuer.",
-    "Power level must be positive integer.": "Le niveau d'autorité doit être un entier positif.",
+    "Power level must be positive integer.": "Le rang doit être un entier positif.",
     "Privileged Users": "Utilisateurs privilégiés",
     "Profile": "Profil",
     "Reason": "Raison",
@@ -151,10 +151,10 @@
     "%(senderName)s removed their profile picture.": "%(senderName)s a supprimé son image de profil.",
     "%(senderName)s requested a VoIP conference.": "%(senderName)s a demandé une téléconférence audio.",
     "Return to login screen": "Retourner à l’écran de connexion",
-    "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s n’a pas la permission de vous envoyer des notifications - merci de vérifier les paramètres de votre navigateur",
-    "%(brand)s was not given permission to send notifications - please try again": "%(brand)s n’a pas reçu la permission de vous envoyer des notifications - veuillez réessayer",
+    "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s n’a pas l’autorisation de vous envoyer des notifications - merci de vérifier les paramètres de votre navigateur",
+    "%(brand)s was not given permission to send notifications - please try again": "%(brand)s n’a pas reçu l’autorisation de vous envoyer des notifications - veuillez réessayer",
     "%(brand)s version:": "Version de %(brand)s :",
-    "Room %(roomId)s not visible": "Le salon %(roomId)s n'est pas visible",
+    "Room %(roomId)s not visible": "Le salon %(roomId)s n’est pas visible",
     "Room Colour": "Couleur du salon",
     "Rooms": "Salons",
     "Search": "Rechercher",
@@ -165,7 +165,7 @@
     "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s a invité %(targetDisplayName)s à rejoindre le salon.",
     "Server error": "Erreur du serveur",
     "Server may be unavailable, overloaded, or search timed out :(": "Le serveur semble être inaccessible, surchargé ou la recherche a expiré :(",
-    "Server may be unavailable, overloaded, or you hit a bug.": "Le serveur semble être indisponible, surchargé ou vous avez rencontré un problème.",
+    "Server may be unavailable, overloaded, or you hit a bug.": "Le serveur semble être indisponible, surchargé ou vous êtes tombé sur un bug.",
     "Server unavailable, overloaded, or something else went wrong.": "Le serveur semble être inaccessible, surchargé ou quelque chose s'est mal passé.",
     "Session ID": "Identifiant de session",
     "%(senderName)s set a profile picture.": "%(senderName)s a défini une image de profil.",
@@ -175,7 +175,7 @@
     "Sign in": "Se connecter",
     "Sign out": "Se déconnecter",
     "%(count)s of your messages have not been sent.|other": "Certains de vos messages n’ont pas été envoyés.",
-    "Someone": "Quelqu'un",
+    "Someone": "Quelqu’un",
     "Submit": "Soumettre",
     "Success": "Succès",
     "This email address is already in use": "Cette adresse e-mail est déjà utilisée",
@@ -183,7 +183,7 @@
     "The email address linked to your account must be entered.": "L’adresse e-mail liée à votre compte doit être renseignée.",
     "The remote side failed to pick up": "Le correspondant n’a pas décroché",
     "This room has no local addresses": "Ce salon n'a pas d'adresse locale",
-    "This room is not recognised.": "Ce salon n'est pas reconnu.",
+    "This room is not recognised.": "Ce salon n’est pas reconnu.",
     "This doesn't appear to be a valid email address": "Cette adresse e-mail ne semble pas valide",
     "This phone number is already in use": "Ce numéro de téléphone est déjà utilisé",
     "This room is not accessible by remote Matrix servers": "Ce salon n’est pas accessible par les serveurs Matrix distants",
@@ -195,8 +195,8 @@
     "Unable to verify email address.": "Impossible de vérifier l’adresse e-mail.",
     "Unban": "Révoquer le bannissement",
     "%(senderName)s unbanned %(targetName)s.": "%(senderName)s a révoqué le bannissement de %(targetName)s.",
-    "Unable to capture screen": "Impossible de faire une capture d'écran",
-    "Unable to enable Notifications": "Impossible d'activer les notifications",
+    "Unable to capture screen": "Impossible de faire une capture d’écran",
+    "Unable to enable Notifications": "Impossible d’activer les notifications",
     "Unmute": "Activer le son",
     "Upload avatar": "Télécharger une photo de profil",
     "Upload Failed": "Échec de l’envoi",
@@ -208,19 +208,19 @@
     "Voice call": "Appel vocal",
     "VoIP conference finished.": "Téléconférence VoIP terminée.",
     "VoIP conference started.": "Téléconférence VoIP démarrée.",
-    "VoIP is unsupported": "Voix sur IP non gérée",
+    "VoIP is unsupported": "Voix sur IP non prise en charge",
     "Warning!": "Attention !",
     "Who can access this room?": "Qui peut accéder au salon ?",
     "Who can read history?": "Qui peut lire l'historique ?",
     "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s a annulé l’invitation de %(targetName)s.",
     "You are already in a call.": "Vous avez déjà un appel en cours.",
-    "You cannot place a call with yourself.": "Vous ne pouvez pas passer d'appel avec vous-même.",
-    "You cannot place VoIP calls in this browser.": "Vous ne pouvez pas passer d'appel en Voix sur IP dans ce navigateur.",
+    "You cannot place a call with yourself.": "Vous ne pouvez pas passer d’appel avec vous-même.",
+    "You cannot place VoIP calls in this browser.": "Vous ne pouvez pas passer d’appel en Voix sur IP dans ce navigateur.",
     "You do not have permission to post to this room": "Vous n’avez pas la permission de poster dans ce salon",
     "You have no visible notifications": "Vous n'avez pas de notification visible",
-    "You need to be able to invite users to do that.": "Vous devez être capable d’inviter des utilisateurs pour faire ça.",
+    "You need to be able to invite users to do that.": "Vous devez avoir l’autorisation d’inviter des utilisateurs pour faire ceci.",
     "You need to be logged in.": "Vous devez être identifié.",
-    "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Votre adresse e-mail ne semble pas être associée à un identifiant Matrix sur ce serveur d'accueil.",
+    "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Votre adresse e-mail ne semble pas être associée à un identifiant Matrix sur ce serveur d’accueil.",
     "You seem to be in a call, are you sure you want to quit?": "Vous semblez avoir un appel en cours, voulez-vous vraiment partir ?",
     "You seem to be uploading files, are you sure you want to quit?": "Vous semblez être en train d'envoyer des fichiers, voulez-vous vraiment partir ?",
     "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Vous ne pourrez pas annuler cette modification car vous promouvez l’utilisateur au même rang que le vôtre.",
@@ -377,7 +377,7 @@
     "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (rang %(powerLevelNumber)s)",
     "(could not connect media)": "(impossible de se connecter au média)",
     "(no answer)": "(pas de réponse)",
-    "(unknown failure: %(reason)s)": "(erreur inconnue : %(reason)s)",
+    "(unknown failure: %(reason)s)": "(erreur inconnue : %(reason)s)",
     "Your browser does not support the required cryptography extensions": "Votre navigateur ne supporte pas les extensions cryptographiques nécessaires",
     "Not a valid %(brand)s keyfile": "Fichier de clé %(brand)s non valide",
     "Authentication check failed: incorrect password?": "Erreur d’authentification : mot de passe incorrect ?",
@@ -388,13 +388,13 @@
     "Add a widget": "Ajouter un widget",
     "Allow": "Autoriser",
     "Delete widget": "Supprimer le widget",
-    "Define the power level of a user": "Définir le rang d'un utilisateur",
+    "Define the power level of a user": "Définir le rang d’un utilisateur",
     "Edit": "Modifier",
     "Enable automatic language detection for syntax highlighting": "Activer la détection automatique de la langue pour la correction orthographique",
     "To get started, please pick a username!": "Pour commencer, choisissez un nom d'utilisateur !",
     "Unable to create widget.": "Impossible de créer le widget.",
-    "You are not in this room.": "Vous n'êtes pas dans ce salon.",
-    "You do not have permission to do that in this room.": "Vous n'avez pas la permission d'effectuer cette action dans ce salon.",
+    "You are not in this room.": "Vous n’êtes pas dans ce salon.",
+    "You do not have permission to do that in this room.": "Vous n’avez pas l’autorisation d’effectuer cette action dans ce salon.",
     "Example": "Exemple",
     "Create": "Créer",
     "Featured Rooms:": "Salons mis en avant :",
@@ -411,21 +411,21 @@
     "Copied!": "Copié !",
     "Failed to copy": "Échec de la copie",
     "%(widgetName)s widget modified by %(senderName)s": "Widget %(widgetName)s modifié par %(senderName)s",
-    "Who would you like to add to this community?": "Qui souhaitez-vous ajouter à cette communauté ?",
-    "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Attention : toute personne ajoutée à une communauté sera visible par tous ceux connaissant l'identifiant de la communauté",
-    "Invite new community members": "Inviter de nouveaux membres dans cette communauté",
-    "Which rooms would you like to add to this community?": "Quels salons souhaitez-vous ajouter à cette communauté ?",
+    "Who would you like to add to this community?": "Qui souhaitez-vous ajouter à cette communauté ?",
+    "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Attention : toute personne ajoutée à une communauté sera visible par tous ceux connaissant l’identifiant de la communauté",
+    "Invite new community members": "Inviter de nouvelles personnes dans cette communauté",
+    "Which rooms would you like to add to this community?": "Quels salons souhaitez-vous ajouter à cette communauté ?",
     "Add rooms to the community": "Ajouter des salons à la communauté",
     "Add to community": "Ajouter à la communauté",
-    "Failed to invite the following users to %(groupId)s:": "Échec de l'invitation des utilisateurs à %(groupId)s :",
-    "Failed to invite users to community": "Échec de l'invitation d'utilisateurs à la communauté",
-    "Failed to invite users to %(groupId)s": "Échec de l'invitation d'utilisateurs à %(groupId)s",
-    "Failed to add the following rooms to %(groupId)s:": "Échec de l'ajout des salons suivants à %(groupId)s :",
+    "Failed to invite the following users to %(groupId)s:": "Échec de l’invitation des utilisateurs suivants à %(groupId)s :",
+    "Failed to invite users to community": "Échec de l’invitation des utilisateurs à la communauté",
+    "Failed to invite users to %(groupId)s": "Échec de l’invitation des utilisateurs à %(groupId)s",
+    "Failed to add the following rooms to %(groupId)s:": "Échec de l’ajout des salons suivants à %(groupId)s :",
     "Ignored user": "Utilisateur ignoré",
-    "You are now ignoring %(userId)s": "Dorénavant vous ignorez %(userId)s",
-    "Unignored user": "Utilisateur n'étant plus ignoré",
-    "You are no longer ignoring %(userId)s": "Vous n'ignorez plus %(userId)s",
-    "Invite to Community": "Inviter dans la Communauté",
+    "You are now ignoring %(userId)s": "Vous ignorez désormais %(userId)s",
+    "Unignored user": "L’utilisateur n’est plus ignoré",
+    "You are no longer ignoring %(userId)s": "Vous n’ignorez plus %(userId)s",
+    "Invite to Community": "Inviter dans la communauté",
     "Communities": "Communautés",
     "Message Pinning": "Épingler un message",
     "Mention": "Mentionner",
@@ -573,7 +573,7 @@
     "Mirror local video feed": "Inverser horizontalement la vidéo locale (effet miroir)",
     "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Un e-mail a été envoyé à %(emailAddress)s. Après avoir suivi le lien présent dans celui-ci, cliquez ci-dessous.",
     "Ignores a user, hiding their messages from you": "Ignore un utilisateur, en masquant ses messages",
-    "Stops ignoring a user, showing their messages going forward": "N'ignore plus un utilisateur, en affichant ses messages à partir de maintenant",
+    "Stops ignoring a user, showing their messages going forward": "Arrête d’ignorer un utilisateur, en affichant ses messages à partir de maintenant",
     "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "La visibilité de \"%(roomName)s\" dans %(groupId)s n'a pas pu être mise à jour.",
     "Visibility in Room List": "Visibilité dans la liste des salons",
     "Visible to everyone": "Visible pour tout le monde",
@@ -609,7 +609,7 @@
     "Display your community flair in rooms configured to show it.": "Sélectionnez les badges dans les paramètres de chaque salon pour les afficher.",
     "expand": "développer",
     "collapse": "réduire",
-    "Call Failed": "L'appel a échoué",
+    "Call Failed": "L’appel a échoué",
     "Send": "Envoyer",
     "Old cryptography data detected": "Anciennes données de chiffrement détectées",
     "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Nous avons détecté des données d'une ancienne version de %(brand)s. Le chiffrement de bout en bout n'aura pas fonctionné correctement sur l'ancienne version. Les messages chiffrés échangés récemment dans l'ancienne version ne sont peut-être pas déchiffrables dans cette version. Les échanges de message avec cette version peuvent aussi échouer. Si vous rencontrez des problèmes, déconnectez-vous puis reconnectez-vous. Pour conserver l'historique des messages, exportez puis réimportez vos clés de chiffrement.",
@@ -625,13 +625,13 @@
     "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Le respect de votre vie privée est important pour nous, donc nous ne collectons aucune donnée personnelle ou permettant de vous identifier pour nos statistiques.",
     "Learn more about how we use analytics.": "En savoir plus sur notre utilisation des statistiques.",
     "The information being sent to us to help make %(brand)s better includes:": "Les informations qui nous sont envoyées et qui nous aident à améliorer %(brand)s comportent :",
-    "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Si la page contient des informations permettant de vous identifier, comme un salon, un identifiant d'utilisateur ou de groupe, ces données sont enlevées avant qu'elle ne soit envoyée au serveur.",
+    "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Si la page contient des informations permettant de vous identifier, comme un salon, un identifiant d’utilisateur ou de groupe, ces données sont enlevées avant qu’elle ne soit envoyée au serveur.",
     "The platform you're on": "La plateforme que vous utilisez",
     "The version of %(brand)s": "La version de %(brand)s",
     "Your language of choice": "La langue que vous avez choisie",
-    "Which officially provided instance you are using, if any": "L'instance officielle que vous utilisez, si vous en utilisez une",
-    "Whether or not you're using the Richtext mode of the Rich Text Editor": "Si vous utilisez le mode « texte enrichi » de l'éditeur de texte enrichi",
-    "Your homeserver's URL": "L'URL de votre serveur d'accueil",
+    "Which officially provided instance you are using, if any": "L’instance officielle que vous utilisez, si vous en utilisez une",
+    "Whether or not you're using the Richtext mode of the Rich Text Editor": "Si vous utilisez le mode « texte enrichi » de l’éditeur de texte enrichi",
+    "Your homeserver's URL": "L’URL de votre serveur d’accueil",
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s %(day)s %(monthName)s %(fullYear)s",
     "This room is not public. You will not be able to rejoin without an invite.": "Ce salon n'est pas public. Vous ne pourrez pas y revenir sans invitation.",
     "Community IDs cannot be empty.": "Les identifiants de communauté ne peuvent pas être vides.",
@@ -648,7 +648,7 @@
     "Code": "Code",
     "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Si vous avez signalé un bug via GitHub, les journaux de débogage peuvent nous aider à identifier le problème. Les journaux de débogage contiennent des données d'utilisation de l'application dont votre nom d'utilisateur, les identifiants ou alias des salons ou groupes que vous avez visité et les noms d'utilisateur des autres participants. Ils ne contiennent pas les messages.",
     "Submit debug logs": "Envoyer les journaux de débogage",
-    "Opens the Developer Tools dialog": "Ouvre la fenêtre des Outils de développeur",
+    "Opens the Developer Tools dialog": "Ouvre la fenêtre des outils de développeur",
     "Unable to join community": "Impossible de rejoindre la communauté",
     "Unable to leave community": "Impossible de quitter la communauté",
     "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Les changements effectués au <bold1>nom</bold1> et à l'<bold2>avatar</bold2> de votre communauté peuvent prendre jusqu'à 30 minutes avant d'être vus par d'autres utilisateurs.",
@@ -784,7 +784,7 @@
     "Preparing to send logs": "Préparation d'envoi des journaux",
     "Missing roomId.": "Identifiant de salon manquant.",
     "Popout widget": "Détacher le widget",
-    "Every page you use in the app": "Toutes les pages que vous utilisez dans l'application",
+    "Every page you use in the app": "Toutes les pages que vous utilisez dans l’application",
     "e.g. <CurrentPageURL>": "par ex. <CurrentPageURL>",
     "Your device resolution": "La résolution de votre appareil",
     "Always show encryption icons": "Toujours afficher les icônes de chiffrement",
@@ -832,8 +832,8 @@
     "Demote yourself?": "Vous rétrograder ?",
     "Demote": "Rétrograder",
     "This event could not be displayed": "Cet événement n'a pas pu être affiché",
-    "Permission Required": "Permission requise",
-    "You do not have permission to start a conference call in this room": "Vous n'avez pas la permission de lancer un appel en téléconférence dans ce salon",
+    "Permission Required": "Autorisation requise",
+    "You do not have permission to start a conference call in this room": "Vous n’avez pas l’autorisation de lancer un appel en téléconférence dans ce salon",
     "A call is currently being placed!": "Un appel est en cours !",
     "Failed to remove widget": "Échec de la suppression du widget",
     "An error ocurred whilst trying to remove the widget from the room": "Une erreur est survenue lors de la suppression du widget du salon",
@@ -862,8 +862,8 @@
     "Upgrade this room to version %(version)s": "Mettre à niveau ce salon vers la version %(version)s",
     "Forces the current outbound group session in an encrypted room to be discarded": "Force la session de groupe sortante actuelle dans un salon chiffré à être rejetée",
     "Unable to connect to Homeserver. Retrying...": "Impossible de se connecter au serveur d'accueil. Reconnexion...",
-    "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s a défini l'adresse principale pour ce salon comme %(address)s.",
-    "%(senderName)s removed the main address for this room.": "%(senderName)s a supprimé l'adresse principale de ce salon.",
+    "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s a défini l’adresse principale pour ce salon comme %(address)s.",
+    "%(senderName)s removed the main address for this room.": "%(senderName)s a supprimé l’adresse principale de ce salon.",
     "%(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 utilise maintenant 3 à 5 fois moins de mémoire, en ne chargeant les informations des autres utilisateurs que quand elles sont nécessaires. Veuillez patienter pendant que l'on se resynchronise avec le serveur !",
     "Updating %(brand)s": "Mise à jour de %(brand)s",
     "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Avant de soumettre vos journaux, vous devez <a>créer une « issue » sur GitHub</a> pour décrire votre problème.",
@@ -934,7 +934,7 @@
     "Common names and surnames are easy to guess": "Les noms et prénoms répandus sont faciles à deviner",
     "Use a longer keyboard pattern with more turns": "Utilisez un schéma plus long et avec plus de variations",
     "Failed to load group members": "Échec du chargement des membres du groupe",
-    "Failed to invite users to the room:": "Échec de l'invitation d'utilisateurs dans le salon :",
+    "Failed to invite users to the room:": "Échec de l’invitation d'utilisateurs dans le salon :",
     "There was an error joining the room": "Une erreur est survenue en rejoignant le salon",
     "You do not have permission to invite people to this room.": "Vous n'avez pas la permission d'envoyer des invitations dans ce salon.",
     "User %(user_id)s does not exist": "L'utilisateur %(user_id)s n'existe pas",
@@ -968,14 +968,14 @@
     "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Impossible de trouver les profils pour les identifiants Matrix listés ci-dessous. Voulez-vous quand même les inviter ?",
     "Invite anyway and never warn me again": "Inviter quand même et ne plus me prévenir",
     "Invite anyway": "Inviter quand même",
-    "Whether or not you're logged in (we don't record your username)": "Si vous êtes connecté ou pas (votre nom d'utilisateur n'est pas enregistré)",
+    "Whether or not you're logged in (we don't record your username)": "Si vous êtes connecté ou pas (votre nom d'utilisateur n’est pas enregistré)",
     "Upgrades a room to a new version": "Met à niveau un salon vers une nouvelle version",
     "Sets the room name": "Définit le nom du salon",
     "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s a mis à niveau ce salon.",
     "%(displayName)s is typing …": "%(displayName)s est en train d'écrire…",
-    "%(names)s and %(count)s others are typing …|other": "%(names)s et %(count)s autres sont en train d'écrire…",
-    "%(names)s and %(count)s others are typing …|one": "%(names)s et un autre sont en train d'écrire…",
-    "%(names)s and %(lastPerson)s are typing …": "%(names)s et %(lastPerson)s sont en train d'écrire…",
+    "%(names)s and %(count)s others are typing …|other": "%(names)s et %(count)s autres sont en train d’écrire…",
+    "%(names)s and %(count)s others are typing …|one": "%(names)s et un autre sont en train d’écrire…",
+    "%(names)s and %(lastPerson)s are typing …": "%(names)s et %(lastPerson)s sont en train d’écrire…",
     "Enable Emoji suggestions while typing": "Activer la suggestion d’émojis lors de la saisie",
     "Render simple counters in room header": "Afficher des compteurs simples dans l’en-tête des salons",
     "Show a placeholder for removed messages": "Afficher les messages supprimés",
@@ -1084,7 +1084,7 @@
     "A new recovery passphrase and key for Secure Messages have been detected.": "Un nouveau mot de passe et une nouvelle clé de récupération pour les messages sécurisés ont été détectés.",
     "Recovery Method Removed": "Méthode de récupération supprimée",
     "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si vous n'avez pas supprimé la méthode de récupération, un attaquant peut être en train d'essayer d'accéder à votre compte. Modifiez le mot de passe de votre compte et configurez une nouvelle méthode de récupération dans les réglages.",
-    "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Le fichier \"%(fileName)s\" dépasse la taille limite autorisée par ce serveur pour les téléchargements",
+    "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Le fichier « %(fileName)s » dépasse la taille limite autorisée par ce serveur pour les envois",
     "Gets or sets the room topic": "Récupère ou définit le sujet du salon",
     "This room has no topic.": "Ce salon n'a pas de sujet.",
     "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s a rendu le salon public à tous ceux qui en connaissent le lien.",
@@ -1092,7 +1092,7 @@
     "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s a changé la règle d’adhésion en %(rule)s",
     "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s a autorisé les visiteurs à rejoindre le salon.",
     "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s a empêché les visiteurs de rejoindre le salon.",
-    "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s a changé l'accès des visiteurs en %(rule)s",
+    "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s a changé l’accès des visiteurs en %(rule)s",
     "Group & filter rooms by custom tags (refresh to apply changes)": "Grouper et filtrer les salons grâce à des étiquettes personnalisées (actualiser pour appliquer les changements)",
     "Verify this user by confirming the following emoji appear on their screen.": "Vérifier cet utilisateur en confirmant que les émojis suivant apparaissent sur son écran.",
     "Unable to find a supported verification method.": "Impossible de trouver une méthode de vérification prise en charge.",
@@ -1206,7 +1206,7 @@
     "Create your Matrix account on %(serverName)s": "Créez votre compte Matrix sur %(serverName)s",
     "Could not load user profile": "Impossible de charger le profil de l’utilisateur",
     "Your Matrix account on %(serverName)s": "Votre compte Matrix sur %(serverName)s",
-    "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Ajoute ¯\\_(ツ)_/¯ devant un message en texte brut",
+    "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Ajoute ¯\\_(ツ)_/¯ en préfixe du message",
     "User %(userId)s is already in the room": "L’utilisateur %(userId)s est déjà membre du salon",
     "The user must be unbanned before they can be invited.": "Le bannissement de l’utilisateur doit être révoqué avant de pouvoir l’inviter.",
     "<a>Upgrade</a> to your own domain": "<a>Mettre à niveau</a> vers votre propre domaine",
@@ -1452,7 +1452,7 @@
     "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Si vous ne voulez pas utiliser <server /> pour découvrir et être découvrable par les contacts que vous connaissez, saisissez un autre serveur d’identité ci-dessous.",
     "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "L’utilisation d’un serveur d’identité est optionnelle. Si vous ne choisissez pas d’utiliser un serveur d’identité, les autres utilisateurs ne pourront pas vous découvrir et vous ne pourrez pas en inviter par e-mail ou par téléphone.",
     "Do not use an identity server": "Ne pas utiliser de serveur d’identité",
-    "You do not have the required permissions to use this command.": "Vous n’avez pas les permissions nécessaires pour utiliser cette commande.",
+    "You do not have the required permissions to use this command.": "Vous n’avez pas les autorisations nécessaires pour utiliser cette commande.",
     "Upgrade the room": "Mettre à niveau le salon",
     "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Renseignez un e-mail pour la récupération de compte. Utilisez un e-mail ou un téléphone pour être éventuellement découvrable par des contacts existants.",
     "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Renseignez un e-mail pour la récupération de compte. Utilisez un e-mail pour être éventuellement découvrable par des contacts existants.",
@@ -1580,7 +1580,7 @@
     "Unread messages.": "Messages non lus.",
     "Show tray icon and minimize window to it on close": "Afficher l’icône dans la barre d’état et minimiser la fenêtre lors de la fermeture",
     "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Cette action nécessite l’accès au serveur d’identité par défaut <server /> afin de valider une adresse e-mail ou un numéro de téléphone, mais le serveur n’a aucune condition de service.",
-    "Trust": "Confiance",
+    "Trust": "Faire confiance",
     "Message Actions": "Actions de message",
     "%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
     "You verified %(name)s": "Vous avez vérifié %(name)s",
@@ -1665,9 +1665,9 @@
     "Verification Request": "Demande de vérification",
     "Match system theme": "S’adapter au thème du système",
     "%(senderName)s placed a voice call.": "%(senderName)s a passé un appel vocal.",
-    "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s a passé un appel vocal. (pas pris en charge par ce navigateur)",
+    "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s a passé un appel vocal. (non pris en charge par ce navigateur)",
     "%(senderName)s placed a video call.": "%(senderName)s a passé un appel vidéo.",
-    "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s a passé un appel vidéo. (pas pris en charge par ce navigateur)",
+    "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s a passé un appel vidéo. (non pris en charge par ce navigateur)",
     "Clear notifications": "Vider les notifications",
     "Customise your experience with experimental labs features. <a>Learn more</a>.": "Personnalisez votre expérience avec des fonctionnalités expérimentales du labo. <a>En savoir plus</a>.",
     "Error upgrading room": "Erreur lors de la mise à niveau du salon",
@@ -1838,7 +1838,7 @@
     "Unknown (user, session) pair:": "Paire (utilisateur, session) inconnue :",
     "Session already verified!": "Session déjà vérifiée !",
     "WARNING: Session already verified, but keys do NOT MATCH!": "ATTENTION : La session a déjà été vérifiée mais les clés NE CORRESPONDENT PAS !",
-    "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ATTENTION : ÉCHEC DE LA VÉRIFICATION DE CLÉ ! La clé de signature pour %(userId)s et la session %(deviceId)s est « %(fprint)s » que ne correspond pas à la clé fournie « %(fingerprint)s ». Cela pourrait signifier que vos communications sont interceptées !",
+    "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ATTENTION : ÉCHEC DE LA VÉRIFICATION DE CLÉ ! La clé de signature pour %(userId)s et la session %(deviceId)s est « %(fprint)s  ce qui ne correspond pas à la clé fournie « %(fingerprint)s ». Cela pourrait signifier que vos communications sont interceptées !",
     "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "La clé de signature que vous avez fournie correspond à celle que vous avez reçue de la session %(deviceId)s de %(userId)s. Session marquée comme vérifiée.",
     "Never send encrypted messages to unverified sessions from this session": "Ne jamais envoyer de messages chiffrés aux sessions non vérifiées depuis cette session",
     "Never send encrypted messages to unverified sessions in this room from this session": "Ne jamais envoyer des messages chiffrés aux sessions non vérifiées dans ce salon depuis cette session",
@@ -1940,7 +1940,7 @@
     "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) s’est connecté à une nouvelle session sans la vérifier :",
     "Ask this user to verify their session, or manually verify it below.": "Demandez à cet utilisateur de vérifier sa session, ou vérifiez-la manuellement ci-dessous.",
     "Verify by scanning": "Vérifier en scannant",
-    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Si vous utilisez %(brand)s sur un appareil où le toucher est le mécanisme primaire de saisie",
+    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Si vous utilisez %(brand)s sur un appareil où le tactile est le mode principal de saisie",
     "Whether you're using %(brand)s as an installed Progressive Web App": "Si vous utilisez %(brand)s en tant qu’application web progressive (PWA)",
     "Your user agent": "Votre agent utilisateur",
     "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "La session que vous essayez de vérifier ne prend pas en charge les codes QR ou la vérification d’émojis, qui sont les méthodes prises en charge par %(brand)s. Essayez avec un autre client.",
@@ -1960,7 +1960,7 @@
     "The internet connection either session is using": "La connection internet de l'une des sessions",
     "We recommend you change your password and recovery key in Settings immediately": "Nous vous recommandons de changer votre mot de passe et la clé de récupération dans Paramètres dès que possible",
     "Sign In or Create Account": "Se connecter ou créer un compte",
-    "Use your account or create a new one to continue.": "Utilisez votre compte ou créez un nouveau compte pour continuer.",
+    "Use your account or create a new one to continue.": "Utilisez votre compte ou créez en un pour continuer.",
     "Create Account": "Créer un compte",
     "Order rooms by name": "Trier les salons par nom",
     "Show rooms with unread notifications first": "Afficher en premier les salons avec des notifications non lues",
@@ -2378,7 +2378,7 @@
     "%(brand)s Desktop": "%(brand)s Desktop",
     "%(brand)s iOS": "%(brand)s iOS",
     "%(brand)s X for Android": "%(brand)s X pour Android",
-    "Are you sure you want to cancel entering passphrase?": "Souhaitez-vous vraiment annuler l'entrée de la phrase de passe ?",
+    "Are you sure you want to cancel entering passphrase?": "Souhaitez-vous vraiment annuler la saisie de la phrase de passe ?",
     "Unexpected server error trying to leave the room": "Erreur de serveur inattendue en essayant de quitter le salon",
     "Error leaving room": "Erreur en essayant de quitter le salon",
     "The person who invited you already left the room.": "La personne vous ayant invité a déjà quitté le salon.",
@@ -2400,18 +2400,18 @@
     "The operation could not be completed": "L'opération n'a pas pu être terminée",
     "Failed to save your profile": "Erreur lors de l'enregistrement du profile",
     "Unknown App": "Application inconnue",
-    "%(senderName)s declined the call.": "%(senderName)s a refusé l'appel.",
+    "%(senderName)s declined the call.": "%(senderName)s a refusé l’appel.",
     "(an error occurred)": "(une erreur est survenue)",
     "(connection failed)": "(échec de connexion)",
-    "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s a changé les paramètres d'accès du serveur pour ce salon.",
-    "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s a défini les paramètres d'accès du serveur pour ce salon.",
-    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Ajoute ( ͡° ͜ʖ ͡°) devant un message en texte brut",
+    "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s a changé les paramètres d’accès du serveur pour ce salon.",
+    "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s a défini les paramètres d’accès du serveur pour ce salon.",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Ajoute ( ͡° ͜ʖ ͡°) en préfixe du message",
     "This will end the conference for everyone. Continue?": "Ceci arrêtera la téléconférence pour tout le monde. Continuer ?",
     "End conference": "Finir la téléconférence",
-    "The call was answered on another device.": "L'appel a été répondu sur un autre appareil.",
+    "The call was answered on another device.": "L’appel a été répondu sur un autre appareil.",
     "Answered Elsewhere": "Répondu autre-part",
-    "The call could not be established": "L'appel n'a pas pu être établi",
-    "The other party declined the call.": "L'autre personne a décliné l'appel.",
+    "The call could not be established": "L’appel n’a pas pu être établi",
+    "The other party declined the call.": "Le correspondant a décliné l’appel.",
     "Call Declined": "Appel rejeté",
     "Ignored attempt to disable encryption": "Essai de désactiver le chiffrement ignoré",
     "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Ajoutez les utilisateurs et les serveurs que vous voulez ignorer ici. Utilisez des astérisques pour que %(brand)s comprenne tous les caractères. Par exemple, <code>@bot:*</code> va ignorer tous les utilisateurs ayant le nom 'bot' sur n'importe quel serveur.",
@@ -2428,8 +2428,8 @@
     "Offline encrypted messaging using dehydrated devices": "Messagerie hors-ligne chiffrée utilisant des appareils déshydratés",
     "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Prototype de communautés v2. Requiert un serveur d'accueil compatible. Très expérimental - à utiliser avec précaution.",
     "Safeguard against losing access to encrypted messages & data": "Sécurité contre la perte d'accès aux messages & données chiffrées",
-    "(their device couldn't start the camera / microphone)": "(son appareil ne peut pas démarrer la caméra / le microphone)",
-    "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Tous les serveurs sont interdits de participation ! Ce salon ne peut plus être utilisé.",
+    "(their device couldn't start the camera / microphone)": "(leur appareil ne peut pas démarrer la caméra/le microphone)",
+    "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Tous les serveurs ont été bannis ! Ce salon ne peut plus être utilisé.",
     "What's the name of your community or team?": "Quel est le nom de votre communauté ou équipe ?",
     "You can change this later if needed.": "Vous pouvez modifier ceci après si besoin.",
     "Use this when referencing your community to others. The community ID cannot be changed.": "Utilisez ceci lorsque vous faites référence à votre communauté aux autres. L'identifiant de la communauté ne peut pas être modifié.",
@@ -2570,12 +2570,12 @@
     "Afghanistan": "Afghanistan",
     "United States": "États-Unis",
     "United Kingdom": "Royaume-Uni",
-    "You've reached the maximum number of simultaneous calls.": "Vous avez atteint le nombre maximum d'appels en simultané.",
-    "No other application is using the webcam": "Aucune autre application n'est en train d'utiliser la caméra",
-    "A microphone and webcam are plugged in and set up correctly": "Un microphone et une caméra sont branchées et bien configurées",
-    "Unable to access webcam / microphone": "Impossible d'accéder à la caméra ou microphone",
-    "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "La fonction a échoué car le microphone n'a pas pu être accédé. Vérifiez qu'un microphone est branché et bien configuré.",
-    "Unable to access microphone": "Impossible d'accéder au microphone",
+    "You've reached the maximum number of simultaneous calls.": "Vous avez atteint le nombre maximum d’appels en simultané.",
+    "No other application is using the webcam": "Aucune autre application n’est en train d’utiliser la caméra",
+    "A microphone and webcam are plugged in and set up correctly": "Un microphone et une caméra sont branchées et bien configurés",
+    "Unable to access webcam / microphone": "Impossible d’accéder à la caméra ou au microphone",
+    "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "La fonction a échoué faute de pouvoir accéder au microphone. Vérifiez qu’un microphone est branché et bien configuré.",
+    "Unable to access microphone": "Impossible d’accéder au microphone",
     "Belgium": "Belgique",
     "Belarus": "Biélorussie",
     "Barbados": "Barbade",
@@ -2591,15 +2591,15 @@
     "Antigua & Barbuda": "Antigue-et-Barbude",
     "Antarctica": "Antarctique",
     "Anguilla": "Anguilla",
-    "Angola": "République d'Angola",
+    "Angola": "République d’Angola",
     "Andorra": "Andorre",
     "American Samoa": "Samoa américaines",
     "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invitez quelqu'un via leur nom, e-mail ou nom d'utilisateur (p. ex. <userId/>) ou <a>partagez ce salon</a>.",
     "Start a conversation with someone using their name, email address or username (like <userId/>).": "Commencer une conversation avec quelqu'un via leur nom, e-mail ou nom d'utilisateur (comme par exemple <userId/>).",
-    "Too Many Calls": "Trop d'appels",
-    "Permission is granted to use the webcam": "Permission accordée pour l'utilisation de la webcam",
-    "Call failed because webcam or microphone could not be accessed. Check that:": "La fonction a échoué car la webcam ou le microphone ne pouvait pas être accédé. Vérifiez que :",
-    "Send stickers to this room as you": "Envoyer des stickers dans ce salon en tant que vous",
+    "Too Many Calls": "Trop d’appels",
+    "Permission is granted to use the webcam": "L’autorisation d’accéder à la caméra a été accordée",
+    "Call failed because webcam or microphone could not be accessed. Check that:": "La fonction a échoué faute de pouvoir accéder à la caméra ou au microphone. Vérifiez que :",
+    "Send stickers to this room as you": "Envoyer des stickers dans ce salon en tant que vous-même",
     "Zambia": "Zambie",
     "Yemen": "Yémen",
     "Western Sahara": "Sahara occidental",
@@ -2685,7 +2685,7 @@
     "North Korea": "Corée du Nord",
     "Norfolk Island": "Île Norfolk",
     "Niue": "Niue",
-    "Nigeria": "Nigéria",
+    "Nigeria": "Nigeria",
     "Niger": "Niger",
     "Nicaragua": "Nicaragua",
     "New Zealand": "Nouvelle-Zélande",
@@ -2703,7 +2703,7 @@
     "Monaco": "Monaco",
     "Moldova": "Moldavie",
     "Micronesia": "États fédérés de Micronésie",
-    "Mexico": "Mexico",
+    "Mexico": "Mexique",
     "Mayotte": "Mayotte",
     "Mauritius": "République de Maurice",
     "Mauritania": "Mauritanie",
@@ -2783,13 +2783,13 @@
     "Equatorial Guinea": "Guinée équatoriale",
     "El Salvador": "Le Salvador",
     "Egypt": "Égypte",
-    "Ecuador": "République de l'Équateur",
+    "Ecuador": "République de l’Équateur",
     "Dominican Republic": "République dominicaine",
     "Dominica": "Dominique",
     "Djibouti": "Djibouti",
     "Denmark": "Danemark",
-    "Côte d’Ivoire": "Côte d’Ivoire (Terre d'Éburnie)",
-    "Czech Republic": "La République tchèque",
+    "Côte d’Ivoire": "Côte d’Ivoire",
+    "Czech Republic": "République tchèque",
     "Cyprus": "Chypre",
     "Curaçao": "Curaçao",
     "Cuba": "Cuba",
@@ -2799,7 +2799,7 @@
     "Congo - Kinshasa": "République démocratique du Congo",
     "Congo - Brazzaville": "République du Congo",
     "Comoros": "Comores",
-    "Colombia": "Colombia",
+    "Colombia": "Colombie",
     "Cocos (Keeling) Islands": "îles Cocos",
     "Christmas Island": "île Christmas",
     "China": "Chine",
@@ -2812,12 +2812,12 @@
     "Canada": "Canada",
     "Cameroon": "Cameroun",
     "Cambodia": "Cambodge",
-    "Burundi": "La république du Burundi",
+    "Burundi": "République du Burundi",
     "Burkina Faso": "Burkina Faso",
     "Bulgaria": "Bulgarie",
     "Brunei": "Brunéi",
     "British Virgin Islands": "Îles Vierges britanniques",
-    "British Indian Ocean Territory": "Territoire britannique de l'océan Indien",
+    "British Indian Ocean Territory": "Territoire britannique de l’océan Indien",
     "Brazil": "Brésil",
     "Bouvet Island": "Île Bouvet",
     "Botswana": "Botswana",
@@ -2825,14 +2825,14 @@
     "Bolivia": "Bolivie",
     "Bhutan": "Bhoutan",
     "Bermuda": "Bermudes",
-    "with state key %(stateKey)s": "avec la ou les clés d'état %(stateKey)s",
-    "with an empty state key": "avec une clé d'état vide",
-    "See when anyone posts a sticker to your active room": "Voir quand n'importe qui envoye un sticker dans le salon actuel",
+    "with state key %(stateKey)s": "avec la ou les clés d’état %(stateKey)s",
+    "with an empty state key": "avec une clé d’état vide",
+    "See when anyone posts a sticker to your active room": "Voir quand n’importe qui envoie un sticker dans le salon actuel",
     "See when a sticker is posted in this room": "Voir quand un sticker est envoyé dans ce salon",
-    "See when the avatar changes in your active room": "Voir quand l'avatar change dans le salon actuel",
-    "Change the avatar of your active room": "Changer l'avatar du salon actuel",
-    "See when the avatar changes in this room": "Voir quand l'avatar change dans ce salon",
-    "Change the avatar of this room": "Changer l'avatar de ce salon",
+    "See when the avatar changes in your active room": "Voir quand l’avatar change dans le salon actuel",
+    "Change the avatar of your active room": "Changer l’avatar du salon actuel",
+    "See when the avatar changes in this room": "Voir quand l’avatar change dans ce salon",
+    "Change the avatar of this room": "Changer l’avatar de ce salon",
     "Send stickers into your active room": "Envoyer des stickers dans le salon actuel",
     "See when the topic changes in this room": "Voir quand le sujet change dans ce salon",
     "See when the topic changes in your active room": "Voir quand le sujet change dans le salon actuel",
@@ -2842,35 +2842,35 @@
     "Change the topic of your active room": "Changer le sujet dans le salon actuel",
     "Change the topic of this room": "Changer le sujet de ce salon",
     "Send stickers into this room": "Envoyer des stickers dans ce salon",
-    "Remain on your screen when viewing another room, when running": "Reste sur votre écran quand vous regardez un autre salon lors de l'appel",
-    "Takes the call in the current room off hold": "Reprends l'appel en cours dans ce salon",
-    "Places the call in the current room on hold": "Met l'appel en pause dans ce salon",
-    "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Ajoute \"(╯°□°)╯︵ ┻━┻\" en préfixe du message",
-    "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Ajoute \"┬──┬ ノ( ゜-゜ノ)\" en préfixe du message",
+    "Remain on your screen when viewing another room, when running": "Reste sur votre écran quand vous regardez un autre salon lors de l’appel",
+    "Takes the call in the current room off hold": "Reprend l’appel en cours dans ce salon",
+    "Places the call in the current room on hold": "Met l’appel en pause dans ce salon",
+    "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Ajoute (╯°□°)╯︵ ┻━┻ en préfixe du message",
+    "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Ajoute ┬──┬ ノ( ゜-゜ノ) en préfixe du message",
     "Effects": "Effets",
     "Zimbabwe": "Zimbabwe",
     "Send images as you in your active room": "Envoie des images sous votre nom dans le salon actuel",
     "Send images as you in this room": "Envoie des images sous votre nom dans ce salon",
-    "See emotes posted to your active room": "Voir les émoticônes envoyées dans le salon actuel",
-    "See emotes posted to this room": "Voir les émoticônes envoyées dans ce salon",
-    "Send emotes as you in your active room": "Envoyer des émoticônes sous votre nom dans le salon actuel",
-    "Send emotes as you in this room": "Envoyer des émoticônes sous votre nom dans ce salon",
-    "See videos posted to your active room": "Voir les vidéos publiées dans votre salon actuel",
+    "See emotes posted to your active room": "Voir les réactions envoyées dans le salon actuel",
+    "See emotes posted to this room": "Voir les réactions envoyées dans ce salon",
+    "Send emotes as you in your active room": "Envoyer des réactions sous votre nom dans le salon actuel",
+    "Send emotes as you in this room": "Envoyer des réactions sous votre nom dans ce salon",
+    "See videos posted to your active room": "Voir les vidéos publiées dans votre salon actif",
     "See videos posted to this room": "Voir les vidéos publiées dans ce salon",
-    "Send videos as you in your active room": "Envoie des vidéos en tant que vous dans votre salon actuel",
+    "Send videos as you in your active room": "Envoie des vidéos en tant que vous-même dans votre salon actuel",
     "Send videos as you in this room": "Envoie des vidéos en tant que vous dans ce salon",
     "See images posted to this room": "Voir les images publiées dans ce salon",
-    "See images posted to your active room": "Voir les images publiées dans votre salon actuel",
-    "See messages posted to your active room": "Voir les messages publiés dans votre salon actuel",
+    "See images posted to your active room": "Voir les images publiées dans votre salon actif",
+    "See messages posted to your active room": "Voir les messages publiés dans votre salon actif",
     "See messages posted to this room": "Voir les messages publiés dans ce salon",
-    "Send messages as you in your active room": "Envoie des messages en tant que vous dans votre salon actuel",
-    "Send messages as you in this room": "Envoie des messages en tant que vous dans ce salon",
+    "Send messages as you in your active room": "Envoie des messages en tant que vous-même dans votre salon actif",
+    "Send messages as you in this room": "Envoie des messages en tant que vous-même dans ce salon",
     "The <b>%(capability)s</b> capability": "La capacité <b>%(capability)s</b>",
     "See <b>%(eventType)s</b> events posted to your active room": "Voir les événements <b>%(eventType)s</b> publiés dans votre salon actuel",
-    "Send <b>%(eventType)s</b> events as you in your active room": "Envoie des événements <b>%(eventType)s</b> en tant que vous dans votre salon actuel",
+    "Send <b>%(eventType)s</b> events as you in your active room": "Envoie des événements <b>%(eventType)s</b> en tant que vous-même dans votre salon actuel",
     "See <b>%(eventType)s</b> events posted to this room": "Voir les événements <b>%(eventType)s</b> publiés dans ce salon",
-    "Send <b>%(eventType)s</b> events as you in this room": "Envoie des événements <b>%(eventType)s</b> en tant que vous dans ce salon",
-    "Send stickers to your active room as you": "Envoie des stickers en tant que vous dans le salon actuel",
+    "Send <b>%(eventType)s</b> events as you in this room": "Envoie des événements <b>%(eventType)s</b> en tant que vous-même dans ce salon",
+    "Send stickers to your active room as you": "Envoie des stickers en tant que vous-même dans le salon actuel",
     "Continue with %(ssoButtons)s": "Continuer avec %(ssoButtons)s",
     "About homeservers": "À propos des serveurs d'accueils",
     "Learn more": "En savoir plus",
@@ -2930,12 +2930,12 @@
     "Don't miss a reply": "Ne ratez pas une réponse",
     "See <b>%(msgtype)s</b> messages posted to your active room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans le salon actuel",
     "See <b>%(msgtype)s</b> messages posted to this room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans ce salon",
-    "Send <b>%(msgtype)s</b> messages as you in this room": "Envoie des messages de type<b>%(msgtype)s</b> en tant que vous dans ce salon",
-    "Send <b>%(msgtype)s</b> messages as you in your active room": "Envoie des messages de type <b>%(msgtype)s</b> en tant que vous dans votre salon actuel",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Envoie des messages de type<b>%(msgtype)s</b> en tant que vous-même dans ce salon",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "Envoie des messages de type <b>%(msgtype)s</b> en tant que vous-même dans votre salon actif",
     "See general files posted to your active room": "Voir les fichiers postés dans votre salon actuel",
     "See general files posted to this room": "Voir les fichiers postés dans ce salon",
-    "Send general files as you in your active room": "Envoie des fichiers en tant que vous dans votre salon actuel",
-    "Send general files as you in this room": "Envoie des fichiers en tant que vous dans ce salon",
+    "Send general files as you in your active room": "Envoyer des fichiers en tant que vous-même dans votre salon actif",
+    "Send general files as you in this room": "Envoyer des fichiers en tant que vous-même dans ce salon",
     "Search (must be enabled)": "Recherche (si activée)",
     "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Cette session a détecté que votre phrase de passe et clé de sécurité pour les messages sécurisés ont été supprimées.",
     "A new Security Phrase and key for Secure Messages have been detected.": "Une nouvelle phrase de passe et clé pour les messages sécurisés ont été détectées.",
@@ -3035,10 +3035,10 @@
     "See when the name changes in your active room": "Suivre les changements de nom dans le salon actif",
     "Change which room, message, or user you're viewing": "Changer le salon, message, ou la personne que vous visualisez",
     "Change which room you're viewing": "Changer le salon que vous visualisez",
-    "Remain on your screen while running": "Restez sur votre écran pendant l’exécution",
+    "Remain on your screen while running": "Reste sur votre écran pendant l’exécution",
     "%(senderName)s has updated the widget layout": "%(senderName)s a mis à jour la disposition du widget",
-    "Converts the DM to a room": "Transformer le message privé en salon",
-    "Converts the room to a DM": "Transformer le salon en message privé",
+    "Converts the DM to a room": "Transforme le message privé en salon",
+    "Converts the room to a DM": "Transforme le salon en message privé",
     "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Votre serveur d’accueil a rejeté la demande de connexion. Ceci pourrait être dû à une connexion qui prend trop de temps. Si cela persiste, merci de contacter l’administrateur de votre serveur d’accueil.",
     "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Votre serveur d’accueil n’est pas accessible, nous n’avons pas pu vous connecter. Merci de réessayer. Si cela persiste, merci de contacter l’administrateur de votre serveur d’accueil.",
     "Try again": "Réessayez",

From 83d10b827b0f75decc8959adb7c833fee5406afe Mon Sep 17 00:00:00 2001
From: Jean-Luc KABORE-TURQUIN <turquinjl@gmail.com>
Date: Thu, 25 Feb 2021 08:15:23 +0000
Subject: [PATCH 209/389] Translated using Weblate (French)

Currently translated at 99.2% (2758 of 2780 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 304599cd44..3d024f6e18 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -77,7 +77,7 @@
     "Email": "E-mail",
     "Failed to unban": "Échec de la révocation du bannissement",
     "Failed to verify email address: make sure you clicked the link in the email": "La vérification de l’adresse e-mail a échoué : vérifiez que vous avez bien cliqué sur le lien dans l’e-mail",
-    "Failure to create room": "Échec de la création du salon",
+    "Failure to create room": "Échec de création du salon",
     "Favourites": "Favoris",
     "Fill screen": "Plein écran",
     "Filter room members": "Filtrer les membres du salon",

From f75d98493d0fba3883cf355746d63e2ee6df6bba Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Wed, 24 Feb 2021 21:44:12 +0000
Subject: [PATCH 210/389] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2780 of 2780 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 | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 398dd04ec2..d0f8cc3b69 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -3060,5 +3060,27 @@
     "Failed to connect to your homeserver. Please close this dialog and try again.": "A matrix szerverhez való csatlakozás nem sikerült. Zárja be ezt az ablakot és próbálja újra.",
     "Abort": "Megszakítás",
     "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Biztos benne, hogy meg kívánja szakítani a gazdagép létrehozásának a folyamatát? A folyamat nem folytatható.",
-    "Confirm abort of host creation": "Erősítse meg a gazdagép készítés megszakítását"
+    "Confirm abort of host creation": "Erősítse meg a gazdagép készítés megszakítását",
+    "Upgrade to %(hostSignupBrand)s": "Frissítés erre: %(hostSignupBrand)s",
+    "Edit Values": "Értékek szerkesztése",
+    "Values at explicit levels in this room:": "Egyedi szinthez tartozó értékek ebben a szobában:",
+    "Values at explicit levels:": "Egyedi szinthez tartozó értékek:",
+    "Value in this room:": "Érték ebben a szobában:",
+    "Value:": "Érték:",
+    "Save setting values": "Beállított értékek mentése",
+    "Values at explicit levels in this room": "Egyedi szinthez tartozó értékek ebben a szobában",
+    "Values at explicit levels": "Egyedi szinthez tartozó értékek",
+    "Settable at room": "Szobára beállítható",
+    "Settable at global": "Általánosan beállítható",
+    "Level": "Szint",
+    "Setting definition:": "Beállítás leírása:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Ez a felület nem ellenőrzi az érték típusát. Csak saját felelősségére használja.",
+    "Caution:": "Figyelmeztetés:",
+    "Setting:": "Beállítás:",
+    "Value in this room": "Érték ebben a szobában",
+    "Value": "Érték",
+    "Setting ID": "Beállítás azon.",
+    "Failed to save settings": "A beállítások elmentése nem sikerült",
+    "Settings Explorer": "Beállítás Böngésző",
+    "Show chat effects (animations when receiving e.g. confetti)": "Csevegés effektek megjelenítése (mint a konfetti animáció)"
 }

From 32ac7d7c8b98e26872bef1b707d05187d89405f4 Mon Sep 17 00:00:00 2001
From: jelv <post@jelv.nl>
Date: Thu, 25 Feb 2021 13:56:38 +0000
Subject: [PATCH 211/389] Translated using Weblate (Dutch)

Currently translated at 100.0% (2780 of 2780 strings)

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

diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 101d997d9c..0d80e520c8 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -689,7 +689,7 @@
     "Messages in one-to-one chats": "Berichten in één-op-één gesprekken",
     "Unavailable": "Niet beschikbaar",
     "View Decrypted Source": "Ontsleutelde bron bekijken",
-    "Failed to update keywords": "Bijwerken van trefwoorden is mislukt",
+    "Failed to update keywords": "Updaten van trefwoorden is mislukt",
     "remove %(name)s from the directory.": "verwijder %(name)s uit de catalogus.",
     "Notifications on the following keywords follow rules which can’t be displayed here:": "Meldingen op de volgende trefwoorden volgen regels die hier niet getoond kunnen worden:",
     "Please set a password!": "Stel een wachtwoord in!",
@@ -740,7 +740,7 @@
     "What's new?": "Wat is er nieuw?",
     "Notify me for anything else": "Stuur een melding voor al het andere",
     "When I'm invited to a room": "Wanneer ik uitgenodigd word in een gesprek",
-    "Can't update user notification settings": "Kan de meldingsinstellingen van de gebruiker niet bijwerken",
+    "Can't update user notification settings": "Kan de meldingsinstellingen van de gebruiker niet updaten",
     "Notify for all other messages/rooms": "Stuur een melding voor alle andere berichten/gesprekken",
     "Unable to look up room ID from server": "Kon de gesprek-ID niet van de server ophalen",
     "Couldn't find a matching Matrix room": "Kon geen bijbehorend Matrix-gesprek vinden",
@@ -852,7 +852,7 @@
     "Gets or sets the room topic": "Verkrijgt het onderwerp van het gesprek of stelt het in",
     "This room has no topic.": "Dit gesprek heeft geen onderwerp.",
     "Sets the room name": "Stelt de gespreksnaam in",
-    "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s heeft dit gesprek bijgewerkt.",
+    "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s heeft dit gesprek geüpgraded.",
     "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s heeft het gesprek toegankelijk gemaakt voor iedereen die de koppeling kent.",
     "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s heeft het gesprek enkel op uitnodiging toegankelijk gemaakt.",
     "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s heeft de toegangsregel veranderd naar ‘%(rule)s’",
@@ -1240,7 +1240,7 @@
     "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s heeft de uitnodiging aan %(targetDisplayName)s toe te treden tot het gesprek ingetrokken.",
     "Upgrade this room to the recommended room version": "Werk dit gesprek bij tot de aanbevolen versie",
     "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "Dit gesprek draait op groepsgespreksversie <roomVersion />, die door deze homeserver als <i>onstabiel</i> is gemarkeerd.",
-    "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Bijwerken zal de huidige versie van dit gesprek sluiten, en onder dezelfde naam een bijgewerkte versie starten.",
+    "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgraden zal de huidige versie van dit gesprek sluiten, en onder dezelfde naam een geüpgraded versie starten.",
     "Failed to revoke invite": "Intrekken van uitnodiging is mislukt",
     "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Kon de uitnodiging niet intrekken. De server ondervindt mogelijk een tijdelijk probleem, of u heeft niet het recht de uitnodiging in te trekken.",
     "Revoke invite": "Uitnodiging intrekken",
@@ -1289,7 +1289,7 @@
     "Unexpected error resolving homeserver configuration": "Onverwachte fout bij het controleren van de homeserverconfiguratie",
     "The user's homeserver does not support the version of the room.": "De homeserver van de gebruiker biedt geen ondersteuning voor de gespreksversie.",
     "Show hidden events in timeline": "Verborgen gebeurtenissen op de tijdslijn weergeven",
-    "When rooms are upgraded": "Wanneer gesprekken bijgewerkt worden",
+    "When rooms are upgraded": "Wanneer gesprekken geüpgraded worden",
     "this room": "dit gesprek",
     "View older messages in %(roomName)s.": "Bekijk oudere berichten in %(roomName)s.",
     "Joining room …": "Deelnemen aan gesprek…",
@@ -1316,7 +1316,7 @@
     "This room doesn't exist. Are you sure you're at the right place?": "Dit gesprek bestaat niet. Weet u zeker dat u zich op de juiste plaats bevindt?",
     "Try again later, or ask a room admin to check if you have access.": "Probeer het later opnieuw, of vraag een gespreksbeheerder om te controleren of u wel toegang heeft.",
     "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "De foutcode %(errcode)s is weergegeven bij het toetreden van het gesprek. Als u meent dat u dit bericht foutief te zien krijgt, gelieve dan <issueLink>een foutmelding in te dienen</issueLink>.",
-    "This room has already been upgraded.": "Dit gesprek is reeds bijgewerkt.",
+    "This room has already been upgraded.": "Dit gesprek is reeds geüpgraded.",
     "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>heeft gereageerd met %(shortName)s</reactedWith>",
     "edited": "bewerkt",
     "Rotate Left": "Links draaien",
@@ -1557,7 +1557,7 @@
     "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Dit vergt validatie van een e-mailadres of telefoonnummer middels de standaardidentiteitsserver <server />, maar die server heeft geen gebruiksvoorwaarden.",
     "Trust": "Vertrouwen",
     "Custom (%(level)s)": "Aangepast (%(level)s)",
-    "Error upgrading room": "Bijwerken van gesprek mislukt",
+    "Error upgrading room": "Upgraden van gesprek mislukt",
     "Double check that your server supports the room version chosen and try again.": "Ga nogmaals na dat de server de gekozen gespreksversie ondersteunt, en probeer het dan opnieuw.",
     "Verifies a user, session, and pubkey tuple": "Verifieert een combinatie van gebruiker+sessie+publieke sleutel",
     "Unknown (user, session) pair:": "Onbekende combinatie gebruiker+sessie:",
@@ -1618,7 +1618,7 @@
     "Lock": "Hangslot",
     "Verify yourself & others to keep your chats safe": "Verifieer uzelf en anderen om uw gesprekken veilig te houden",
     "Other users may not trust it": "Mogelijk wantrouwen anderen het",
-    "Upgrade": "Bijwerken",
+    "Upgrade": "Upgraden",
     "Verify": "Verifiëren",
     "Later": "Later",
     "Review": "Controle",
@@ -2377,7 +2377,7 @@
     "Use between %(min)s pt and %(max)s pt": "Gebruik een getal tussen %(min)s pt en %(max)s pt",
     "Custom font size can only be between %(min)s pt and %(max)s pt": "Aangepaste lettergrootte kan alleen een getal tussen %(min)s pt en %(max)s pt zijn",
     "Size must be a number": "Grootte moet een getal zijn",
-    "New version available. <a>Update now.</a>": "Nieuwe versie beschikbaar. <a>Nu bijwerken.</a>",
+    "New version available. <a>Update now.</a>": "Nieuwe versie beschikbaar. <a>Nu updaten.</a>",
     "not ready": "Niet gereed",
     "ready": "Gereed",
     "unexpected type": "Onverwacht type",
@@ -2445,7 +2445,7 @@
     "The person who invited you already left the room, or their server is offline.": "De persoon door wie u ben uitgenodigd heeft het gesprek al verlaten, of hun server is offline.",
     "The person who invited you already left the room.": "De persoon door wie u ben uitgenodigd, heeft het gesprek reeds verlaten.",
     "New version of %(brand)s is available": "Nieuwe versie van %(brand)s is beschikbaar",
-    "Update %(brand)s": "%(brand)s bijwerken",
+    "Update %(brand)s": "%(brand)s updaten",
     "%(senderName)s has updated the widget layout": "%(senderName)s heeft de widget-indeling bijgewerkt",
     "%(senderName)s declined the call.": "%(senderName)s heeft de oproep afgewezen.",
     "(an error occurred)": "(een fout is opgetreden)",
@@ -2950,5 +2950,27 @@
     "Create a Group Chat": "Maak een groepsgesprek aan",
     "Send a Direct Message": "Start een direct gesprek",
     "Welcome to %(appName)s": "Welkom bij %(appName)s",
-    "<a>Add a topic</a> to help people know what it is about.": "<a>Stel een gespreksonderwerp in</a> zodat mensen weten waar het over gaat."
+    "<a>Add a topic</a> to help people know what it is about.": "<a>Stel een gespreksonderwerp in</a> zodat mensen weten waar het over gaat.",
+    "Upgrade to %(hostSignupBrand)s": "Upgrade naar %(hostSignupBrand)s",
+    "Edit Values": "Waarde wijzigen",
+    "Values at explicit levels in this room:": "Waarde op expliciete niveaus in dit gesprek:",
+    "Values at explicit levels:": "Waardes op expliciete niveaus:",
+    "Value in this room:": "Waarde in dit gesprek:",
+    "Value:": "Waarde:",
+    "Save setting values": "Instelling waardes opslaan",
+    "Values at explicit levels in this room": "Waardes op expliciete niveaus in dit gesprek",
+    "Values at explicit levels": "Waardes op expliciete niveaus",
+    "Settable at room": "Instelbaar op gesprek",
+    "Settable at global": "Instelbaar op globaal",
+    "Level": "Niveau",
+    "Setting definition:": "Instelling definitie:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "De UI heeft GEEN controle op het type van de waardes. Gebruik op eigen risico.",
+    "Caution:": "Opgelet:",
+    "Setting:": "Instelling:",
+    "Value in this room": "Waarde van dit gesprek",
+    "Value": "Waarde",
+    "Setting ID": "Instellingen-ID",
+    "Failed to save settings": "Kan geen instellingen opslaan",
+    "Settings Explorer": "Instellingen Ontdekken",
+    "Show chat effects (animations when receiving e.g. confetti)": "Effecten tonen (animaties bij ontvangst bijv. confetti)"
 }

From abbe51660438ad2baa92d88097a2c1f926ab71b9 Mon Sep 17 00:00:00 2001
From: Marcelo Filho <marceloaof@protonmail.com>
Date: Fri, 26 Feb 2021 17:32:14 +0000
Subject: [PATCH 212/389] Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2780 of 2780 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 | 38 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 37 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index b32fbf5094..ef748c5fd8 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -2983,5 +2983,41 @@
     "Converts the DM to a room": "Converte a conversa para uma sala",
     "Converts the room to a DM": "Converte a sala para uma conversa",
     "Try again": "Tente novamente",
-    "We couldn't log you in": "Não foi possível fazer login"
+    "We couldn't log you in": "Não foi possível fazer login",
+    "%(hostSignupBrand)s Setup": "Configuração do %(hostSignupBrand)s",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Ao continuar, você permite que o processo de configuração do %(hostSignupBrand)s acesse a sua conta para obter endereços de e-mail verificados, temporariamente. Esses dados não são armazenados.",
+    "Settable at room": "Definido em cada sala",
+    "Settable at global": "Definido globalmente",
+    "Settings Explorer": "Explorador de configurações",
+    "Level": "Nível",
+    "Setting definition:": "Definição da configuração:",
+    "Setting ID": "ID da configuração",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Seu servidor local recusou a sua tentativa de login. Isso pode ocorrer quando a conexão de internet estiver demorando muito. Por favor, tente novamente. Se o problema continuar, entre em contato com o administrador do seu servidor local.",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "O seu servidor local está inacessível e não foi possível fazer o seu login. Tente novamente. Se o problema continuar, entre em contato com o administrador do seu servidor local.",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Anteriormente, pedimos ao seu navegador para lembrar qual servidor local você usa para fazer login, mas infelizmente o navegador se esqueceu disso. Vá para a página de login e tente novamente.",
+    "Upgrade to %(hostSignupBrand)s": "Atualizar para %(hostSignupBrand)s",
+    "Minimize dialog": "Minimizar a janela",
+    "Maximize dialog": "Maximizar a janela",
+    "You should know": "Você deveria saber",
+    "Cookie Policy": "Política de cookies",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Falha ao se conectar ao seu servidor local. Feche esta caixa de diálogo e tente novamente.",
+    "Abort": "Cancelar",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Tem certeza de que deseja cancelar a criação do host? O processo não pode ser continuado.",
+    "Confirm abort of host creation": "Confirmar o cancelamento da criação do host",
+    "Edit Values": "Editar valores",
+    "Values at explicit levels in this room:": "Valores em níveis explícitos nessa sala:",
+    "Values at explicit levels:": "Valores em níveis explícitos:",
+    "Value in this room:": "Valor nessa sala:",
+    "Value:": "Valor:",
+    "Save setting values": "Salvar valores de configuração",
+    "Values at explicit levels in this room": "Valores em níveis explícitos nessa sala",
+    "Values at explicit levels": "Valores em níveis explícitos",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Esta interface de usuário NÃO verifica os tipos de valores. Use por sua conta e risco.",
+    "Caution:": "Atenção:",
+    "Setting:": "Configuração:",
+    "Value in this room": "Valor nessa sala",
+    "Value": "Valor",
+    "Failed to save settings": "Falha ao salvar as configurações",
+    "Show chat effects (animations when receiving e.g. confetti)": "Mostrar efeitos na conversa (por exemplo: animações ao receber confetes)",
+    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "O Element Web é experimental para dispositivos móveis. Para uma melhor experiência e os recursos mais recentes, use nosso aplicativo gratuito."
 }

From 3540bec046cc648f97699967439d81b1c5ed2027 Mon Sep 17 00:00:00 2001
From: rkfg <rkfg@rkfg.me>
Date: Sat, 27 Feb 2021 11:02:52 +0000
Subject: [PATCH 213/389] Translated using Weblate (Russian)

Currently translated at 99.6% (2769 of 2780 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 | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 65aeaf82d9..8310676e69 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -1591,7 +1591,7 @@
     "Error upgrading room": "Ошибка обновления комнаты",
     "Match system theme": "Тема системы",
     "Show tray icon and minimize window to it on close": "Показать иконку в трее и сворачивать окно при закрытии",
-    "Show typing notifications": "Показывать уведомления о наборе",
+    "Show typing notifications": "Показывать уведомления о наборе текста",
     "Delete %(count)s sessions|other": "Удалить %(count)s сессий",
     "Enable desktop notifications for this session": "Включить уведомления для рабочего стола для этой сессии",
     "Enable audible notifications for this session": "Включить звуковые уведомления для этой сессии",
@@ -3031,7 +3031,7 @@
     "Privacy Policy": "Политика конфиденциальности",
     "Cookie Policy": "Политика использования файлов cookie",
     "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Дополнительную информацию можно найти на страницах <privacyPolicyLink />,<termsOfServiceLink /> и <cookiePolicyLink />.",
-    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Продолжая процесс настройки %(hostSignupBrand)s, вы предоставите доступ к вашей учётной записи для получения проверенных адресов электронной почты. Эти данные не сохраняются.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Продолжая процесс настройки %(hostSignupBrand)s, вы предоставите временный доступ к вашей учётной записи для получения проверенных адресов электронной почты. Эти данные не сохраняются.",
     "Failed to connect to your homeserver. Please close this dialog and try again.": "Не удалось подключиться к домашнему серверу. Закройте это диалоговое окно и попробуйте ещё раз.",
     "Abort": "Отмена",
     "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Вы уверены, что хотите прервать создание хоста? Процесс не может быть продолжен.",
@@ -3072,5 +3072,7 @@
     "Value in this room": "Значение в этой комнате",
     "Value": "Значение",
     "Failed to save settings": "Не удалось сохранить настройки",
-    "Show chat effects (animations when receiving e.g. confetti)": "Показать эффекты чата (анимация при получении, например, конфетти)"
+    "Show chat effects (animations when receiving e.g. confetti)": "Показать эффекты чата (анимация при получении, например, конфетти)",
+    "Caution:": "Предупреждение:",
+    "Settings Explorer": "Обзор настроек"
 }

From 8d68cbdb142ab0f9f513d1b3583490824cbb49be Mon Sep 17 00:00:00 2001
From: A-l-exa-n-d-r <A-l-exa-n-d-r@yandex.ru>
Date: Sat, 27 Feb 2021 08:05:08 +0000
Subject: [PATCH 214/389] Translated using Weblate (Russian)

Currently translated at 99.6% (2769 of 2780 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 | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 8310676e69..7cbcb08167 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -3068,7 +3068,7 @@
     "Settable at room": "Устанавливается для комнаты",
     "Settable at global": "Устанавливается на глобальном уровне",
     "Level": "Уровень",
-    "This UI does NOT check the types of the values. Use at your own risk.": "Этот пользовательский интерфейс НЕ проверяет типы значений. Используйте на свой риск.",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Этот пользовательский интерфейс НЕ проверяет типы значений. Используйте на свой страх и риск.",
     "Value in this room": "Значение в этой комнате",
     "Value": "Значение",
     "Failed to save settings": "Не удалось сохранить настройки",

From b6d4e3fe9a247835d87337282ebc2eba7af26a20 Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Fri, 26 Feb 2021 07:13:31 +0000
Subject: [PATCH 215/389] Translated using Weblate (Swedish)

Currently translated at 100.0% (2780 of 2780 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 | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index 916de9ecce..c8a5a41339 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -2996,5 +2996,27 @@
     "Failed to connect to your homeserver. Please close this dialog and try again.": "Misslyckades att ansluta till din hemserver. Vänligen stäng den här dialogrutan och försök igen.",
     "Abort": "Avbryt",
     "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Är du säker på att du vill avbryta skapande av värden? Processen kan inte fortsättas.",
-    "Confirm abort of host creation": "Bekräfta avbrytning av värdskapande"
+    "Confirm abort of host creation": "Bekräfta avbrytning av värdskapande",
+    "Upgrade to %(hostSignupBrand)s": "Uppgradera till %(hostSignupBrand)s",
+    "Edit Values": "Redigera värden",
+    "Values at explicit levels in this room:": "Värden vid explicita nivåer i det här rummet:",
+    "Values at explicit levels:": "Värden vid explicita nivåer:",
+    "Value in this room:": "Värde i det här rummet:",
+    "Value:": "Värde:",
+    "Save setting values": "Spara inställningsvärden",
+    "Values at explicit levels in this room": "Värden vid explicita nivåer i det här rummet",
+    "Values at explicit levels": "Värden vid explicita nivåer",
+    "Settable at room": "Inställningsbar per rum",
+    "Settable at global": "Inställningsbar globalt",
+    "Level": "Nivå",
+    "Setting definition:": "Inställningsdefinition:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Det här UI:t kontrollerar inte typer för värden. Använd på egen risk.",
+    "Caution:": "Varning:",
+    "Setting:": "Inställning:",
+    "Value in this room": "Värde i det här rummet",
+    "Value": "Värde",
+    "Setting ID": "Inställnings-ID",
+    "Failed to save settings": "Misslyckades att spara inställningar",
+    "Settings Explorer": "Inställningsutforskare",
+    "Show chat effects (animations when receiving e.g. confetti)": "Visa chatteffekter (animeringar när du tar emot t.ex. konfetti)"
 }

From ad38f87287b13eb2ef835781287c16f26169a204 Mon Sep 17 00:00:00 2001
From: linsui <linsui@inbox.lv>
Date: Fri, 26 Feb 2021 14:45:43 +0000
Subject: [PATCH 216/389] Translated using Weblate (Chinese (Simplified))

Currently translated at 80.7% (2245 of 2780 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 | 75 ++++++++++++++++++++++++++++++++++-
 1 file changed, 74 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index 167fea70e1..12fb5e2877 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -2409,5 +2409,78 @@
     "The call was answered on another device.": "在另一台设备上应答了该通话。",
     "The call could not be established": "无法建立通话",
     "The other party declined the call.": "对方拒绝了通话。",
-    "Call Declined": "通话被拒绝"
+    "Call Declined": "通话被拒绝",
+    "(connection failed)": "(连接失败)",
+    "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 所有服务器都已禁止参与!此聊天室不再可用。",
+    "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s 为此聊天室更改了服务器 ACL。",
+    "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s 为此聊天室设置了服务器 ACL。",
+    "Hong Kong": "香港",
+    "Cook Islands": "库克群岛",
+    "Congo - Kinshasa": "刚果金",
+    "Congo - Brazzaville": "刚果布拉柴维尔",
+    "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": "玻利维亚",
+    "Armenia": "亚美尼亚",
+    "Bhutan": "不丹",
+    "Bermuda": "百慕大群岛",
+    "Benin": "贝宁",
+    "Belize": "伯利兹",
+    "Belgium": "比利时",
+    "Belarus": "白俄罗斯",
+    "Barbados": "巴巴多斯",
+    "Bangladesh": "孟加拉国",
+    "Bahrain": "巴林",
+    "Bahamas": "巴哈马",
+    "Azerbaijan": "阿塞拜疆",
+    "Austria": "奥地利",
+    "Australia": "澳大利亚",
+    "Aruba": "阿鲁巴岛",
+    "Argentina": "阿根廷",
+    "Antigua & Barbuda": "安提瓜和巴布达",
+    "Antarctica": "南极洲",
+    "Anguilla": "安圭拉",
+    "Angola": "安哥拉",
+    "Andorra": "安道尔",
+    "American Samoa": "美属萨摩亚",
+    "Algeria": "阿尔及利亚",
+    "Albania": "阿尔巴尼亚",
+    "Åland Islands": "奥兰群岛",
+    "Afghanistan": "阿富汗",
+    "United States": "美国",
+    "United Kingdom": "英国",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "您的主服务器已拒绝您的登入尝试。请重试。如果此情况持续发生,请联系您的主服务器管理员。",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "您的主服务器不可达,无法使您登入。请重试。如果此情况持续发生,请联系您的主服务器管理员。",
+    "Try again": "重试",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "我们已要求浏览器记住您使用的主服务器,但不幸的是您的浏览器已忘记。请前往登录页面重试。",
+    "We couldn't log you in": "我们无法使您登入",
+    "This will end the conference for everyone. Continue?": "这将结束所有人的会议。是否继续?",
+    "End conference": "结束会议",
+    "You've reached the maximum number of simultaneous calls.": "您已达到并行呼叫最大数量。",
+    "Too Many Calls": "太多呼叫",
+    "Call failed because webcam or microphone could not be accessed. Check that:": "通话失败,因为无法访问网络摄像头或麦克风。请检查:",
+    "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "呼叫失败,因为无法访问任何麦克风。 检查是否已插入麦克风并正确设置。",
+    "Answered Elsewhere": "在其他地方已回答"
 }

From c4940d164381449f857a5c88cc9abbc62939a6aa Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Thu, 25 Feb 2021 01:41:18 +0000
Subject: [PATCH 217/389] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2780 of 2780 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 | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index b11ff2274f..64dab74139 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -3068,5 +3068,27 @@
     "Failed to connect to your homeserver. Please close this dialog and try again.": "無法連線到您的家伺服器。請關閉對話框並再試一次。",
     "Abort": "中止",
     "Are you sure you wish to abort creation of the host? The process cannot be continued.": "您確定您想要中止主機建立嗎?流程將無法繼續。",
-    "Confirm abort of host creation": "確認中止主機建立"
+    "Confirm abort of host creation": "確認中止主機建立",
+    "Upgrade to %(hostSignupBrand)s": "升級至 %(hostSignupBrand)s",
+    "Edit Values": "編輯值",
+    "Values at explicit levels in this room:": "此聊天室中明確等級的值:",
+    "Values at explicit levels:": "明確等級的值:",
+    "Value in this room:": "此聊天室中的值:",
+    "Value:": "值:",
+    "Save setting values": "儲存設定值",
+    "Values at explicit levels in this room": "此聊天室中明確等級的值",
+    "Values at explicit levels": "明確等級的值",
+    "Settable at room": "聊天室設定",
+    "Settable at global": "全域設定",
+    "Level": "等級",
+    "Setting definition:": "設定定義:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "此使用者介面不會檢查值的類型。使用風險自負。",
+    "Caution:": "警告:",
+    "Setting:": "設定:",
+    "Value in this room": "此聊天室中的值",
+    "Value": "值",
+    "Setting ID": "設定 ID",
+    "Failed to save settings": "儲存設定失敗",
+    "Settings Explorer": "設定瀏覽程式",
+    "Show chat effects (animations when receiving e.g. confetti)": "顯示聊天效果(當收到如五彩紙屑時顯示動畫)"
 }

From 66cf2075fe9457f5190eb43723efc6cefd327285 Mon Sep 17 00:00:00 2001
From: waclaw66 <waclaw66@seznam.cz>
Date: Fri, 26 Feb 2021 08:30:59 +0000
Subject: [PATCH 218/389] Translated using Weblate (Czech)

Currently translated at 100.0% (2780 of 2780 strings)

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

diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index 3ee9c73399..47a99ab670 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -231,7 +231,7 @@
     "You do not have permission to do that in this room.": "V této místnosti nemáte na toto právo.",
     "You cannot place a call with yourself.": "Nemůžete volat sami sobě.",
     "You cannot place VoIP calls in this browser.": "V tomto prohlížeči nelze vytáčet VoIP hovory.",
-    "You do not have permission to post to this room": "Na přispívání do této místnosti nemáte právo",
+    "You do not have permission to post to this room": "Nemáte oprávnění zveřejňovat příspěvky v této místnosti",
     "Online": "Online",
     "Offline": "Offline",
     "Check for update": "Zkontrolovat aktualizace",
@@ -1409,7 +1409,7 @@
     "Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Odeslat pozvánku pomocí serveru identit. <default>Použít výchozí (%(defaultIdentityServerName)s)</default> nebo přenastavit <settings>Nastavení</settings>.",
     "Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Odeslat pozvánku pomocí serveru identit. Přenastavit v <settings>Nastavení</settings>.",
     "Close dialog": "Zavřít dialog",
-    "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Napište nám prosím co se pokazilo a nebo nám napište issue na GitHub, kde popíšete problém.",
+    "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Dejte nám vědět, prosím, co se pokazilo nebo vytvořte issue na GitHubu, kde problém popište.",
     "Removing…": "Odstaňování…",
     "Clear all data": "Smazat všechna data",
     "Please enter a name for the room": "Zadejte prosím název místnosti",
@@ -1529,7 +1529,7 @@
     "Flags": "Vlajky",
     "Quick Reactions": "Rychlé reakce",
     "Cancel search": "Zrušit hledání",
-    "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Vyrobte prosím <newIssueLink>nové issue</newIssueLink> na GitHubu abychom mohli chybu opravit.",
+    "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Vytvořte prosím <newIssueLink>novou issue</newIssueLink> na GitHubu abychom mohli chybu opravit.",
     "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s neudělali %(count)s krát žádnou změnu",
     "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s neudělali žádnou změnu",
     "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s neudělal(a) %(count)s krát žádnou změnu",
@@ -2981,5 +2981,27 @@
     "Failed to connect to your homeserver. Please close this dialog and try again.": "Připojení k domovskému serveru se nezdařilo. Zavřete toto dialogové okno a zkuste to znovu.",
     "Abort": "Přerušit",
     "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Opravdu chcete přerušit vytváření hostitele? Proces nemůže být navázán.",
-    "Confirm abort of host creation": "Potvrďte přerušení vytváření hostitele"
+    "Confirm abort of host creation": "Potvrďte přerušení vytváření hostitele",
+    "Upgrade to %(hostSignupBrand)s": "Aktualizovat na %(hostSignupBrand)s",
+    "Edit Values": "Upravit hodnoty",
+    "Values at explicit levels in this room:": "Hodnoty na explicitních úrovních v této místnosti:",
+    "Values at explicit levels:": "Hodnoty na explicitních úrovních:",
+    "Value in this room:": "Hodnota v této místnosti:",
+    "Value:": "Hodnota:",
+    "Save setting values": "Uložit hodnoty nastavení",
+    "Values at explicit levels in this room": "Hodnoty na explicitních úrovních v této místnosti",
+    "Values at explicit levels": "Hodnoty na explicitních úrovních",
+    "Settable at room": "Nastavitelné v místnosti",
+    "Settable at global": "Nastavitelné na globální úrovni",
+    "Level": "Úroveň",
+    "Setting definition:": "Definice nastavení:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Toto uživatelské rozhraní NEKONTROLUJE typy hodnot. Použití na vlastní nebezpečí.",
+    "Caution:": "Pozor:",
+    "Setting:": "Nastavení:",
+    "Value in this room": "Hodnota v této místnosti",
+    "Value": "Hodnota",
+    "Setting ID": "ID nastavení",
+    "Failed to save settings": "Nastavení se nepodařilo uložit",
+    "Settings Explorer": "Průzkumník nastavení",
+    "Show chat effects (animations when receiving e.g. confetti)": "Zobrazit efekty chatu (animace např. při přijetí konfet)"
 }

From b6a5b08a86cb466d7e11982c1f0b5a91c53bb5ae Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Thu, 25 Feb 2021 06:09:06 +0000
Subject: [PATCH 219/389] Translated using Weblate (Galician)

Currently translated at 100.0% (2780 of 2780 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 | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index b309f12245..98a984d860 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -3065,5 +3065,27 @@
     "Failed to connect to your homeserver. Please close this dialog and try again.": "Fallou a conexión co teu servidor de inicio. Pecha esta información e inténtao outra vez.",
     "Abort": "Abortar",
     "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Tes a certeza de querer cancelar a creación do servidor? O proceso non pode ser completado.",
-    "Confirm abort of host creation": "Corfirma que cancelas a creación do servidor"
+    "Confirm abort of host creation": "Corfirma que cancelas a creación do servidor",
+    "Upgrade to %(hostSignupBrand)s": "Actualizar a %(hostSignupBrand)s",
+    "Edit Values": "Editar valores",
+    "Values at explicit levels in this room:": "Valores a niveis explícitos nesta sala:",
+    "Values at explicit levels:": "Valores a niveis explícitos:",
+    "Value in this room:": "Valor nesta sala:",
+    "Value:": "Valor:",
+    "Save setting values": "Gardar valores configurados",
+    "Values at explicit levels in this room": "Valores a niveis explícitos nesta sala",
+    "Values at explicit levels": "Valores e niveis explícitos",
+    "Settable at room": "Configurable na sala",
+    "Settable at global": "Configurable como global",
+    "Level": "Nivel",
+    "Setting definition:": "Definición do axuste:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Esta IU non comproba os tipos dos valores. Usa baixo a túa responsabilidade.",
+    "Caution:": "Aviso:",
+    "Setting:": "Axuste:",
+    "Value in this room": "Valor nesta sala",
+    "Value": "Valor",
+    "Setting ID": "ID do axuste",
+    "Failed to save settings": "Non se gardaron os axustes",
+    "Settings Explorer": "Navegar nos axustes",
+    "Show chat effects (animations when receiving e.g. confetti)": "Mostrar efectos no chat (animacións na recepción, ex. confetti)"
 }

From 73421597b63b7f3f266f7fcf94bb4030c70b9a12 Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Fri, 26 Feb 2021 17:16:44 +0000
Subject: [PATCH 220/389] Translated using Weblate (Albanian)

Currently translated at 99.6% (2769 of 2780 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 | 24 ++++++++++++++++++++++--
 1 file changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index e8e47ac81b..dd58154b5a 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -2990,7 +2990,7 @@
     "If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Nëse keni harruar Kyçin tuaj të Sigurisë, mund të <button>ujdisni mundësi të reja rimarrjeje</button>.",
     "Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Kyçi juaj i Sigurisë është <b>kopjuar te e papastra juaj</b>, ngjiteni te:",
     "Confirm your Security Phrase": "Ripohoni Frazën tuaj të Sigurisë",
-    "Secure your backup with a Security Phrase": "Sigurojeni kopjeruajtjen tuaj me një Frazë Sigurie.",
+    "Secure your backup with a Security Phrase": "Sigurojeni kopjeruajtjen tuaj me një Frazë Sigurie",
     "Repeat your Security Phrase...": "Përsëritni Frazën tuaj të Sigurisë…",
     "Please enter your Security Phrase a second time to confirm.": "Ju lutemi, që të ripohohet, rijepeni Frazën tuaj të Sigurisë.",
     "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Ky sesion ka pikasur se Fraza e Sigurisë dhe kyçi juaj për Mesazhe të Sigurt janë hequr.",
@@ -3056,5 +3056,25 @@
     "Failed to connect to your homeserver. Please close this dialog and try again.": "S’u arrit të lidhej me shërbyesin tuaj Home. Ju lutemi, mbylleni këtë dialog dhe riprovoni.",
     "Abort": "Ndërprite",
     "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Jeni i sigurt se doni të ndërpritet krijimi i strehës? Procesi s’mund të vazhdohet.",
-    "Confirm abort of host creation": "Ripohoni ndërprerjen e krijimit të strehës"
+    "Confirm abort of host creation": "Ripohoni ndërprerjen e krijimit të strehës",
+    "Upgrade to %(hostSignupBrand)s": "Përmirësojeni me %(hostSignupBrand)s",
+    "Edit Values": "Përpunoni Vlera",
+    "Values at explicit levels in this room:": "Vlera në nivele shprehimisht në këtë dhomë:",
+    "Values at explicit levels:": "Vlera në nivele shprehimisht:",
+    "Value in this room:": "Vlerë në këtë dhomë:",
+    "Value:": "Vlerë:",
+    "Save setting values": "Ruaj vlera rregullimi",
+    "Values at explicit levels in this room": "Vlera në nivele shprehimisht në këtë dhomë",
+    "Values at explicit levels": "Vlera në nivele shprehimisht",
+    "Settable at room": "I caktueshëm për dhomën",
+    "Settable at global": "I caktueshëm te të përgjithshmet",
+    "Level": "Nivel",
+    "Setting definition:": "Përkufizim rregullimi:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Kjo UI NUK kontrollon llojet e vlerave. Përdorini me përgjegjësinë tuaj.",
+    "Setting:": "Rregullim:",
+    "Value in this room": "Vlerë në këtë dhomë",
+    "Value": "Vlerë",
+    "Failed to save settings": "S’u arrit të ruhen rregullimet",
+    "Settings Explorer": "Eksplorues Rregullimesh",
+    "Show chat effects (animations when receiving e.g. confetti)": "Shfaq efekte fjalosjeje (animacione kur merren bonbone, për shembull)"
 }

From 55bfe21df0ed8827b7dca40cd580ac91cf202f82 Mon Sep 17 00:00:00 2001
From: Trendyne <eiko@chiru.no>
Date: Thu, 25 Feb 2021 14:25:53 +0000
Subject: [PATCH 221/389] Translated using Weblate (Icelandic)

Currently translated at 14.8% (413 of 2780 strings)

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

diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json
index 6f561331f6..ddb3bbc66d 100644
--- a/src/i18n/strings/is.json
+++ b/src/i18n/strings/is.json
@@ -458,5 +458,7 @@
     "Passphrases must match": "Lykilfrasar verða að stemma",
     "Passphrase must not be empty": "Lykilfrasi má ekki vera auður",
     "Create Account": "Stofna Reikning",
-    "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "vinsamlegast setja upp <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, eða <safariLink>Safari</safariLink> fyrir besta reynsluna."
+    "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "vinsamlegast setja upp <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, eða <safariLink>Safari</safariLink> fyrir besta reynsluna.",
+    "Explore rooms": "Kanna herbergi",
+    "Sign In": "Skrá inn"
 }

From 36ef0be05b028a4e97afac0499491a20f9ae7ab1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Thu, 25 Feb 2021 16:43:43 +0000
Subject: [PATCH 222/389] Translated using Weblate (Estonian)

Currently translated at 100.0% (2780 of 2780 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 | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 7769cf61c8..eb895f5f26 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -3066,5 +3066,27 @@
     "Privacy Policy": "Privaatsuspoliitika",
     "Cookie Policy": "Küpsiste kasutamine",
     "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Lisateavet leiad <privacyPolicyLink />, <termsOfServiceLink /> ja <cookiePolicyLink /> lehtedelt.",
-    "Failed to connect to your homeserver. Please close this dialog and try again.": "Ei õnnestunud ühendada koduserveriga. Palun sulge see aken ja proovi uuesti."
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Ei õnnestunud ühendada koduserveriga. Palun sulge see aken ja proovi uuesti.",
+    "Upgrade to %(hostSignupBrand)s": "Kui soovid, siis võta kasutusele %(hostSignupBrand)s",
+    "Edit Values": "Muuda väärtusi",
+    "Values at explicit levels in this room:": "Väärtused konkreetsel tasemel selles jututoas:",
+    "Values at explicit levels:": "Väärtused konkreetsel tasemel:",
+    "Value in this room:": "Väärtus selles jututoas:",
+    "Value:": "Väärtus:",
+    "Save setting values": "Salvesta seadistuste väärtused",
+    "Values at explicit levels in this room": "Väärtused konkreetsel tasemel selles jututoas",
+    "Values at explicit levels": "Väärtused konkreetsel tasemel",
+    "Settable at room": "Seadistatav jututoa-kohaselt",
+    "Settable at global": "Seadistatav üldiselt",
+    "Level": "Tase",
+    "Setting definition:": "Seadistuse määratlus:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "See kasutajaliides ei oska kontrollida väärtuste tüüpi ja vormingut. Muudatusi teed omal vastutusel.",
+    "Caution:": "Hoiatus:",
+    "Setting:": "Seadistus:",
+    "Value in this room": "Väärtus selles jututoas",
+    "Value": "Väärtus",
+    "Setting ID": "Seadistuse tunnus",
+    "Failed to save settings": "Seadistuste salvestamine ei õnnestunud",
+    "Settings Explorer": "Seadistuste haldur",
+    "Show chat effects (animations when receiving e.g. confetti)": "Näita vestluses edevat graafikat (näiteks kui keegi on saatnud serpentiine)"
 }

From ebb782062ff6cac80615ba97572bda7985085c91 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 1 Mar 2021 12:51:56 +0000
Subject: [PATCH 223/389] Upgrade matrix-js-sdk to 9.8.0

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

diff --git a/package.json b/package.json
index f49b99831f..4efc24f533 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
     "katex": "^0.12.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.20",
-    "matrix-js-sdk": "9.8.0-rc.1",
+    "matrix-js-sdk": "9.8.0",
     "matrix-widget-api": "^0.1.0-beta.13",
     "minimist": "^1.2.5",
     "pako": "^2.0.3",
diff --git a/yarn.lock b/yarn.lock
index bef26e27b4..17b43fb87a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5572,10 +5572,10 @@ mathml-tag-names@^2.1.3:
   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.8.0-rc.1:
-  version "9.8.0-rc.1"
-  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.8.0-rc.1.tgz#229122583bec5971f22a423a4a40d749e07602d9"
-  integrity sha512-Tmo5cdyBBgYcMZMaAavEvtdCsEwr5sYE0RLd6etLOSTxmGRSYpqKvvKQqGsYrogmZYNbx9nNZYYYV2aJkCKcQg==
+matrix-js-sdk@9.8.0:
+  version "9.8.0"
+  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.8.0.tgz#d71d8c777d2fea3dbc9a050060e4f1a74217dca6"
+  integrity sha512-QKRsnmId53upz4oMhQzm119lT0EcST2SZhnKRRFyykxZI0x7qSulnTTUwztpS/g9yZuZqy7PTVUTHOE2caX5IQ==
   dependencies:
     "@babel/runtime" "^7.12.5"
     another-json "^0.2.0"

From 24d249555c17ffd685e004be5e10da3f64c2cf3f Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 1 Mar 2021 13:09:11 +0000
Subject: [PATCH 224/389] Prepare changelog for v3.15.0

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e727adabfa..c31eedf93b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,19 @@
+Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0)
+
+## Security notice
+
+matrix-react-sdk 3.15.0 fixes a low severity issue (CVE-2021-21320) where the
+user content sandbox can be abused to trick users into opening unexpected
+documents. The content is opened with a `blob` origin that cannot access Matrix
+user data, so messages and secrets are not at risk. Thanks to @keerok for
+responsibly disclosing this via Matrix's Security Disclosure Policy.
+
+## All changes
+
+ * Upgrade to JS SDK 9.8.0
+
 Changes in [3.15.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0-rc.1) (2021-02-24)
 ===============================================================================================================
 [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0...v3.15.0-rc.1)

From 9ab92e7971ae830fe77cc12a0a249ee8794f456c Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 1 Mar 2021 13:09:12 +0000
Subject: [PATCH 225/389] v3.15.0

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

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

From 5a5816d723ec8d4eaf879db84e01b56d0a0eade3 Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 1 Mar 2021 13:10:32 +0000
Subject: [PATCH 226/389] Resetting package fields for development

---
 package.json | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json
index a7c41171e2..732a4e307e 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
     "matrix-gen-i18n": "scripts/gen-i18n.js",
     "matrix-prune-i18n": "scripts/prune-i18n.js"
   },
-  "main": "./lib/index.js",
+  "main": "./src/index.js",
   "matrix_src_main": "./src/index.js",
   "matrix_lib_main": "./lib/index.js",
   "matrix_lib_typings": "./lib/index.d.ts",
@@ -189,6 +189,5 @@
     "transformIgnorePatterns": [
       "/node_modules/(?!matrix-js-sdk).+$"
     ]
-  },
-  "typings": "./lib/index.d.ts"
+  }
 }

From 588e94e8137025b1fcd31a2ac401bfd18b823e9a Mon Sep 17 00:00:00 2001
From: RiotRobot <releases@riot.im>
Date: Mon, 1 Mar 2021 13:10:43 +0000
Subject: [PATCH 227/389] 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 732a4e307e..93f438410e 100644
--- a/package.json
+++ b/package.json
@@ -80,7 +80,7 @@
     "katex": "^0.12.0",
     "linkifyjs": "^2.1.9",
     "lodash": "^4.17.20",
-    "matrix-js-sdk": "9.8.0",
+    "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
     "matrix-widget-api": "^0.1.0-beta.13",
     "minimist": "^1.2.5",
     "pako": "^2.0.3",
diff --git a/yarn.lock b/yarn.lock
index 17b43fb87a..82a4878f39 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5572,10 +5572,9 @@ mathml-tag-names@^2.1.3:
   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.8.0:
+"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
   version "9.8.0"
-  resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.8.0.tgz#d71d8c777d2fea3dbc9a050060e4f1a74217dca6"
-  integrity sha512-QKRsnmId53upz4oMhQzm119lT0EcST2SZhnKRRFyykxZI0x7qSulnTTUwztpS/g9yZuZqy7PTVUTHOE2caX5IQ==
+  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fb73ab687826e4d05fb8b424ab013a771213f84f"
   dependencies:
     "@babel/runtime" "^7.12.5"
     another-json "^0.2.0"

From d679708058befe021f8fba23594180aa76ec4554 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 1 Mar 2021 15:40:46 +0000
Subject: [PATCH 228/389] Fix styling of disabled options in an Iconized menu

---
 res/css/views/context_menus/_IconizedContextMenu.scss | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss
index d911ac6dfe..204435995f 100644
--- a/res/css/views/context_menus/_IconizedContextMenu.scss
+++ b/res/css/views/context_menus/_IconizedContextMenu.scss
@@ -75,6 +75,11 @@ limitations under the License.
                 background-color: $menu-selected-color;
             }
 
+            &.mx_AccessibleButton_disabled {
+                opacity: 0.5;
+                cursor: not-allowed;
+            }
+
             img, .mx_IconizedContextMenu_icon { // icons
                 width: 16px;
                 min-width: 16px;

From ce648633266c53d80148a498b98617f88629a282 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 1 Mar 2021 15:53:16 +0000
Subject: [PATCH 229/389] Annotate User Menu handle with currently selected
 space

---
 res/css/structures/_UserMenu.scss      |  1 +
 src/components/structures/UserMenu.tsx | 40 ++++++++++++++++++++------
 2 files changed, 33 insertions(+), 8 deletions(-)

diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss
index 2a4453df70..3badb0850c 100644
--- a/res/css/structures/_UserMenu.scss
+++ b/res/css/structures/_UserMenu.scss
@@ -72,6 +72,7 @@ limitations under the License.
             position: relative; // to make default avatars work
             margin-right: 8px;
             height: 32px; // to remove the unknown 4px gap the browser puts below it
+            padding: 3px 0; // to align with and without using doubleName
 
             .mx_UserMenu_userAvatar {
                 border-radius: 32px; // should match avatar size
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 5ed6a00d74..b31a5f4b8e 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -15,13 +15,18 @@ limitations under the License.
 */
 
 import React, { createRef } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import classNames from "classnames";
+import * as fbEmitter from "fbemitter";
+
 import { MatrixClientPeg } from "../../MatrixClientPeg";
 import defaultDispatcher from "../../dispatcher/dispatcher";
+import dis from "../../dispatcher/dispatcher";
 import { ActionPayload } from "../../dispatcher/payloads";
 import { Action } from "../../dispatcher/actions";
 import { _t } from "../../languageHandler";
 import { ContextMenuButton } from "./ContextMenu";
-import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
+import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog";
 import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
 import FeedbackDialog from "../views/dialogs/FeedbackDialog";
 import Modal from "../../Modal";
@@ -30,11 +35,10 @@ import SettingsStore from "../../settings/SettingsStore";
 import {getCustomTheme} from "../../theme";
 import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
 import SdkConfig from "../../SdkConfig";
-import {getHomePageUrl} from "../../utils/pages";
+import { getHomePageUrl } from "../../utils/pages";
 import { OwnProfileStore } from "../../stores/OwnProfileStore";
 import { UPDATE_EVENT } from "../../stores/AsyncStore";
 import BaseAvatar from '../views/avatars/BaseAvatar';
-import classNames from "classnames";
 import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
 import { SettingLevel } from "../../settings/SettingLevel";
 import IconizedContextMenu, {
@@ -42,16 +46,16 @@ import IconizedContextMenu, {
     IconizedContextMenuOptionList,
 } from "../views/context_menus/IconizedContextMenu";
 import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
-import * as fbEmitter from "fbemitter";
 import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
 import { showCommunityInviteDialog } from "../../RoomInvite";
-import dis from "../../dispatcher/dispatcher";
 import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
 import ErrorDialog from "../views/dialogs/ErrorDialog";
 import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
-import {UIFeature} from "../../settings/UIFeature";
+import { UIFeature } from "../../settings/UIFeature";
 import HostSignupAction from "./HostSignupAction";
-import {IHostSignupConfig} from "../views/dialogs/HostSignupDialogTypes";
+import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
+import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
+import RoomName from "../views/elements/RoomName";
 
 interface IProps {
     isMinimized: boolean;
@@ -62,6 +66,7 @@ type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
 interface IState {
     contextMenuPosition: PartialDOMRect;
     isDarkTheme: boolean;
+    selectedSpace?: Room;
 }
 
 export default class UserMenu extends React.Component<IProps, IState> {
@@ -79,6 +84,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
         };
 
         OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
+        if (SettingsStore.getValue("feature_spaces")) {
+            SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
+        }
     }
 
     private get hasHomePage(): boolean {
@@ -96,6 +104,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
         if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
         OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
         this.tagStoreRef.remove();
+        if (SettingsStore.getValue("feature_spaces")) {
+            SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
+        }
     }
 
     private onTagStoreUpdate = () => {
@@ -120,6 +131,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
         this.forceUpdate();
     };
 
+    private onSelectedSpaceUpdate = async (selectedSpace?: Room) => {
+        this.setState({ selectedSpace });
+    };
+
     private onThemeChanged = () => {
         this.setState({isDarkTheme: this.isUserOnDarkTheme()});
     };
@@ -517,7 +532,16 @@ export default class UserMenu extends React.Component<IProps, IState> {
                 {/* masked image in CSS */}
             </span>
         );
-        if (prototypeCommunityName) {
+        if (this.state.selectedSpace) {
+            name = (
+                <div className="mx_UserMenu_doubleName">
+                    <span className="mx_UserMenu_userName">{displayName}</span>
+                    <RoomName room={this.state.selectedSpace}>
+                        {(roomName) => <span className="mx_UserMenu_subUserName">{roomName}</span>}
+                    </RoomName>
+                </div>
+            );
+        } else if (prototypeCommunityName) {
             name = (
                 <div className="mx_UserMenu_doubleName">
                     <span className="mx_UserMenu_userName">{prototypeCommunityName}</span>

From 70f1303544c65d46ffb7233259ba33b2ac3d2fa5 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 1 Mar 2021 16:48:35 +0000
Subject: [PATCH 230/389] Fix invite svg viewbox

---
 res/img/element-icons/room/invite.svg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/img/element-icons/room/invite.svg b/res/img/element-icons/room/invite.svg
index 655f9f118a..d2ecb837b2 100644
--- a/res/img/element-icons/room/invite.svg
+++ b/res/img/element-icons/room/invite.svg
@@ -1,3 +1,3 @@
-<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<svg width="24" height="24" viewBox="-0.4 1 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 <path fill-rule="evenodd" clip-rule="evenodd" d="M19.1001 9C18.7779 9 18.5168 8.73883 18.5168 8.41667V6.08333H16.1834C15.8613 6.08333 15.6001 5.82217 15.6001 5.5C15.6001 5.17783 15.8613 4.91667 16.1834 4.91667H18.5168V2.58333C18.5168 2.26117 18.7779 2 19.1001 2C19.4223 2 19.6834 2.26117 19.6834 2.58333V4.91667H22.0168C22.3389 4.91667 22.6001 5.17783 22.6001 5.5C22.6001 5.82217 22.3389 6.08333 22.0168 6.08333H19.6834V8.41667C19.6834 8.73883 19.4223 9 19.1001 9ZM19.6001 11C20.0669 11 20.5212 10.9467 20.9574 10.8458C21.1161 11.5383 21.2 12.2594 21.2 13C21.2 16.1409 19.6917 18.9294 17.3598 20.6808V20.6807C16.0014 21.7011 14.3635 22.3695 12.5815 22.5505C12.2588 22.5832 11.9314 22.6 11.6 22.6C6.29807 22.6 2 18.302 2 13C2 7.69809 6.29807 3.40002 11.6 3.40002C12.3407 3.40002 13.0618 3.48391 13.7543 3.64268C13.6534 4.07884 13.6001 4.53319 13.6001 5C13.6001 8.31371 16.2864 11 19.6001 11ZM11.5999 20.68C13.6754 20.68 15.5585 19.8567 16.9407 18.5189C16.0859 16.4086 14.0167 14.92 11.5998 14.92C9.18298 14.92 7.11378 16.4086 6.25901 18.5189C7.64115 19.8567 9.52436 20.68 11.5999 20.68ZM11.7426 7.41172C10.3168 7.54168 9.2 8.74043 9.2 10.2C9.2 11.7464 10.4536 13 12 13C13.0308 13 13.9315 12.443 14.4176 11.6135C13.0673 10.6058 12.0929 9.12248 11.7426 7.41172Z" fill="black"/>
 </svg>

From ea61b18c1877173cd206734c2926fca04447f0c1 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 1 Mar 2021 17:02:02 +0000
Subject: [PATCH 231/389] Iterate Space Panel alignments

---
 res/css/structures/_SpacePanel.scss           | 99 +++++++++++--------
 src/components/views/spaces/SpacePanel.tsx    | 21 ++--
 .../views/spaces/SpaceTreeLevel.tsx           | 15 ++-
 3 files changed, 81 insertions(+), 54 deletions(-)

diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss
index 8de85f95ef..324757648f 100644
--- a/res/css/structures/_SpacePanel.scss
+++ b/res/css/structures/_SpacePanel.scss
@@ -2,8 +2,6 @@
 Copyright 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
-
-
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
@@ -18,8 +16,12 @@ limitations under the License.
 
 $topLevelHeight: 32px;
 $nestedHeight: 24px;
-$gutterSize: 21px;
+$gutterSize: 17px;
 $activeStripeSize: 4px;
+$activeBorderTransparentGap: 2px;
+
+$activeBackgroundColor: $roomtile-selected-bg-color;
+$activeBorderColor: $secondary-fg-color;
 
 .mx_SpacePanel {
     flex: 0 0 auto;
@@ -68,11 +70,14 @@ $activeStripeSize: 4px;
         cursor: pointer;
     }
 
-    .mx_SpaceItem {
-        position: relative;
-    }
-
     .mx_SpaceItem.collapsed {
+        .mx_SpaceButton {
+            .mx_NotificationBadge {
+                right: -4px;
+                top: -4px;
+            }
+        }
+
         & > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse {
             transform: rotate(-90deg);
         }
@@ -84,20 +89,35 @@ $activeStripeSize: 4px;
 
     .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
         margin-left: $gutterSize;
-
-        &.mx_SpaceButton_active {
-            &::before {
-                left: -$gutterSize;
-            }
-        }
     }
 
     .mx_SpaceButton {
         border-radius: 8px;
         position: relative;
-        margin-bottom: 16px;
+        margin-bottom: 2px;
         display: flex;
         align-items: center;
+        padding: 4px;
+
+        &.mx_SpaceButton_active {
+            &:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper {
+                background-color: $activeBackgroundColor;
+                border-radius: 8px;
+            }
+
+            &.mx_SpaceButton_narrow {
+                .mx_BaseAvatar, .mx_SpaceButton_avatarPlaceholder {
+                    border: 2px $activeBorderColor solid;
+                    border-radius: 11px;
+                }
+            }
+        }
+
+        .mx_SpaceButton_selectionWrapper {
+            display: flex;
+            flex: 1;
+            align-items: center;
+        }
 
         .mx_SpaceButton_name {
             flex: 1;
@@ -107,7 +127,7 @@ $activeStripeSize: 4px;
             max-width: 150px;
             text-overflow: ellipsis;
             overflow: hidden;
-
+            padding-right: 8px;
             font-size: $font-14px;
             line-height: $font-18px;
         }
@@ -123,24 +143,12 @@ $activeStripeSize: 4px;
             mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
         }
 
-        &.mx_SpaceButton_active {
-            &::before {
-                position: absolute;
-                content: '';
-                width: $activeStripeSize;
-                top: 0;
-                left: 0;
-                bottom: 0;
-                background-color: $accent-color;
-                border-radius: 0 4px 4px 0;
-            }
-        }
-
-        .mx_SpaceButton_avatarPlaceholder {
+        .mx_SpaceButton_icon {
             width: $topLevelHeight;
             min-width: $topLevelHeight;
             height: $topLevelHeight;
             border-radius: 8px;
+            position: relative;
 
             &::before {
                 position: absolute;
@@ -155,7 +163,7 @@ $activeStripeSize: 4px;
             }
         }
 
-        &.mx_SpaceButton_home .mx_SpaceButton_avatarPlaceholder {
+        &.mx_SpaceButton_home .mx_SpaceButton_icon {
             background-color: #ffffff;
 
             &::before {
@@ -164,19 +172,28 @@ $activeStripeSize: 4px;
             }
         }
 
-        &.mx_SpaceButton_newCancel .mx_SpaceButton_avatarPlaceholder {
-            background-color: $icon-button-color;
+        .mx_SpaceButton_avatarPlaceholder {
+            border: $activeBorderTransparentGap transparent solid;
+            padding: $activeBorderTransparentGap;
+        }
 
-            &::before {
-                transform: rotate(45deg);
+        .mx_BaseAvatar {
+            /* moving the border-radius to this element from _image
+            element so we can add a border to it without the initials being displaced */
+            overflow: hidden;
+            border: 2px transparent solid;
+            padding: $activeBorderTransparentGap;
+
+            .mx_BaseAvatar_initial {
+                top: $activeBorderTransparentGap;
+                left: $activeBorderTransparentGap;
+            }
+
+            .mx_BaseAvatar_image {
+                border-radius: 8px;
             }
         }
 
-        .mx_BaseAvatar_image {
-            border-radius: 8px;
-        }
-    }
-
     .mx_SpacePanel_badgeContainer {
         height: 16px;
         // don't set width so that it takes no space when there is no badge to show
@@ -201,8 +218,8 @@ $activeStripeSize: 4px;
         .mx_SpaceButton {
             .mx_SpacePanel_badgeContainer {
                 position: absolute;
-                right: -8px;
-                top: -4px;
+                right: 0px;
+                top: 2px;
             }
         }
     }
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index bc9cd5c9fd..760181e0e0 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -56,9 +56,10 @@ const SpaceButton: React.FC<IButtonProps> = ({
 }) => {
     const classes = classNames("mx_SpaceButton", className, {
         mx_SpaceButton_active: selected,
+        mx_SpaceButton_narrow: isNarrow,
     });
 
-    let avatar = <div className="mx_SpaceButton_avatarPlaceholder" />;
+    let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>;
     if (space) {
         avatar = <RoomAvatar width={32} height={32} room={space} />;
     }
@@ -74,18 +75,22 @@ const SpaceButton: React.FC<IButtonProps> = ({
     if (isNarrow) {
         button = (
             <RovingAccessibleTooltipButton className={classes} title={tooltip} onClick={onClick} role="treeitem">
-                { avatar }
-                { notifBadge }
-                { children }
+                <div className="mx_SpaceButton_selectionWrapper">
+                    { avatar }
+                    { notifBadge }
+                    { children }
+                </div>
             </RovingAccessibleTooltipButton>
         );
     } else {
         button = (
             <RovingAccessibleButton className={classes} onClick={onClick} role="treeitem">
-                { avatar }
-                <span className="mx_SpaceButton_name">{ tooltip }</span>
-                { notifBadge }
-                { children }
+                <div className="mx_SpaceButton_selectionWrapper">
+                    { avatar }
+                    <span className="mx_SpaceButton_name">{ tooltip }</span>
+                    { notifBadge }
+                    { children }
+                </div>
             </RovingAccessibleButton>
         );
     }
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index 14fe68ff66..f94798433f 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -95,6 +95,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
         const classes = classNames("mx_SpaceButton", {
             mx_SpaceButton_active: isActive,
             mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
+            mx_SpaceButton_narrow: isNarrow,
         });
         const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
         const childItems = childSpaces && !collapsed ? <SpaceTreeLevel
@@ -129,8 +130,10 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
                     role="treeitem"
                 >
                     { toggleCollapseButton }
-                    <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
-                    { notifBadge }
+                    <div className="mx_SpaceButton_selectionWrapper">
+                        <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
+                        { notifBadge }
+                    </div>
                 </RovingAccessibleTooltipButton>
             );
         } else {
@@ -142,9 +145,11 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
                     role="treeitem"
                 >
                     { toggleCollapseButton }
-                    <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
-                    <span className="mx_SpaceButton_name">{ space.name }</span>
-                    { notifBadge }
+                    <div className="mx_SpaceButton_selectionWrapper">
+                        <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
+                        <span className="mx_SpaceButton_name">{ space.name }</span>
+                        { notifBadge }
+                    </div>
                 </RovingAccessibleButton>
             );
         }

From 89386b9b2ea5d9fc2ef1ff07d0becdc0ad9fc1ff Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 1 Mar 2021 17:06:56 +0000
Subject: [PATCH 232/389] fix missing closing brace

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

diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss
index 324757648f..563c5eba58 100644
--- a/res/css/structures/_SpacePanel.scss
+++ b/res/css/structures/_SpacePanel.scss
@@ -193,6 +193,7 @@ $activeBorderColor: $secondary-fg-color;
                 border-radius: 8px;
             }
         }
+    }
 
     .mx_SpacePanel_badgeContainer {
         height: 16px;

From d731e82fba3819143144d3d64ebb71309284c4df Mon Sep 17 00:00:00 2001
From: Jaiwanth <jaiwanth2011@gmail.com>
Date: Mon, 1 Mar 2021 22:44:48 +0530
Subject: [PATCH 233/389] Fixed edit for markdown images

Signed-off-by: Jaiwanth <jaiwanth2011@gmail.com>
---
 src/editor/deserialize.ts | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts
index 6336b4c46b..bc1dd74c7d 100644
--- a/src/editor/deserialize.ts
+++ b/src/editor/deserialize.ts
@@ -60,6 +60,11 @@ function parseLink(a: HTMLAnchorElement, partCreator: PartCreator) {
     }
 }
 
+function parseImage(img: HTMLImageElement, partCreator: PartCreator) {
+    const { src } = img;
+    return partCreator.plain(`![${img.alt.replace(/[[\\\]]/g, c => "\\" + c)}](${src})`);
+}
+
 function parseCodeBlock(n: HTMLElement, partCreator: PartCreator) {
     const parts = [];
     let language = "";
@@ -102,6 +107,8 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
             return parseHeader(n, partCreator);
         case "A":
             return parseLink(<HTMLAnchorElement>n, partCreator);
+        case "IMG":
+            return parseImage(<HTMLImageElement>n, partCreator);
         case "BR":
             return partCreator.newline();
         case "EM":

From 483d56320c8810ae88dbfd934be3c5d3eea8dee1 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 1 Mar 2021 17:27:09 +0000
Subject: [PATCH 234/389] Beginning of space creation UX from space panel

---
 res/css/_components.scss                      |   2 +
 res/css/structures/_SpacePanel.scss           |  19 ++
 res/css/views/spaces/_SpaceBasicSettings.scss |  86 +++++++++
 res/css/views/spaces/_SpaceCreateMenu.scss    | 138 ++++++++++++++
 res/img/element-icons/lock.svg                |   3 +
 res/img/element-icons/plus.svg                |   3 +
 src/components/structures/ContextMenu.tsx     |   3 +-
 .../views/spaces/SpaceBasicSettings.tsx       | 120 ++++++++++++
 .../views/spaces/SpaceCreateMenu.tsx          | 175 ++++++++++++++++++
 src/components/views/spaces/SpacePanel.tsx    |  21 +++
 src/createRoom.ts                             |   4 +-
 src/i18n/strings/en_EN.json                   |  22 ++-
 12 files changed, 588 insertions(+), 8 deletions(-)
 create mode 100644 res/css/views/spaces/_SpaceBasicSettings.scss
 create mode 100644 res/css/views/spaces/_SpaceCreateMenu.scss
 create mode 100644 res/img/element-icons/lock.svg
 create mode 100644 res/img/element-icons/plus.svg
 create mode 100644 src/components/views/spaces/SpaceBasicSettings.tsx
 create mode 100644 src/components/views/spaces/SpaceCreateMenu.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 29b5262826..8d6597aefa 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -233,6 +233,8 @@
 @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss";
 @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
 @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
+@import "./views/spaces/_SpaceBasicSettings.scss";
+@import "./views/spaces/_SpaceCreateMenu.scss";
 @import "./views/terms/_InlineTermsAgreement.scss";
 @import "./views/toasts/_AnalyticsToast.scss";
 @import "./views/toasts/_NonUrgentEchoFailureToast.scss";
diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss
index 563c5eba58..24d2243912 100644
--- a/res/css/structures/_SpacePanel.scss
+++ b/res/css/structures/_SpacePanel.scss
@@ -177,6 +177,25 @@ $activeBorderColor: $secondary-fg-color;
             padding: $activeBorderTransparentGap;
         }
 
+        &.mx_SpaceButton_new .mx_SpaceButton_icon {
+            background-color: $accent-color;
+            transition: all .1s ease-in-out; // TODO transition
+
+            &::before {
+                background-color: #ffffff;
+                mask-image: url('$(res)/img/element-icons/plus.svg');
+                transition: all .2s ease-in-out; // TODO transition
+            }
+        }
+
+        &.mx_SpaceButton_newCancel .mx_SpaceButton_icon {
+            background-color: $icon-button-color;
+
+            &::before {
+                transform: rotate(45deg);
+            }
+        }
+
         .mx_BaseAvatar {
             /* moving the border-radius to this element from _image
             element so we can add a border to it without the initials being displaced */
diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss
new file mode 100644
index 0000000000..204ccab2b7
--- /dev/null
+++ b/res/css/views/spaces/_SpaceBasicSettings.scss
@@ -0,0 +1,86 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_SpaceBasicSettings {
+    .mx_Field {
+        margin: 32px 0;
+    }
+
+    .mx_SpaceBasicSettings_avatarContainer {
+        display: flex;
+        margin-top: 24px;
+
+        .mx_SpaceBasicSettings_avatar {
+            position: relative;
+            height: 80px;
+            width: 80px;
+            background-color: $tertiary-fg-color;
+            border-radius: 16px;
+        }
+
+        img.mx_SpaceBasicSettings_avatar {
+            width: 80px;
+            height: 80px;
+            object-fit: cover;
+            border-radius: 16px;
+        }
+
+        // only show it when the button is a div and not an img (has avatar)
+        div.mx_SpaceBasicSettings_avatar {
+            cursor: pointer;
+
+            &::before {
+                content: "";
+                position: absolute;
+                height: 80px;
+                width: 80px;
+                top: 0;
+                left: 0;
+                background-color: #ffffff; // white icon fill
+                mask-repeat: no-repeat;
+                mask-position: center;
+                mask-size: 20px;
+                mask-image: url('$(res)/img/element-icons/camera.svg');
+            }
+        }
+
+        > input[type="file"] {
+            display: none;
+        }
+
+        > .mx_AccessibleButton_kind_link {
+            display: inline-block;
+            padding: 0;
+            margin: auto 16px;
+            color: #368bd6;
+        }
+
+        > .mx_SpaceBasicSettings_avatar_remove {
+            color: $notice-primary-color;
+        }
+    }
+
+    .mx_FormButton {
+        padding: 8px 22px;
+        margin-left: auto;
+        display: block;
+        width: min-content;
+    }
+
+    .mx_AccessibleButton_disabled {
+        cursor: not-allowed;
+    }
+}
diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss
new file mode 100644
index 0000000000..2a11ec9f23
--- /dev/null
+++ b/res/css/views/spaces/_SpaceCreateMenu.scss
@@ -0,0 +1,138 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// TODO: the space panel currently does not have a fixed width,
+// just the headers at each level have a max-width of 150px
+// so this will look slightly off for now. We should probably use css grid for the whole main layout...
+$spacePanelWidth: 200px;
+
+.mx_SpaceCreateMenu_wrapper {
+    // background blur everything except SpacePanel
+    .mx_ContextualMenu_background {
+        background-color: $dialog-backdrop-color;
+        opacity: 0.6;
+        left: $spacePanelWidth;
+    }
+
+    .mx_ContextualMenu {
+        padding: 24px;
+        width: 480px;
+        box-sizing: border-box;
+        background-color: $primary-bg-color;
+
+        > div {
+            > h2 {
+                font-weight: $font-semi-bold;
+                font-size: $font-18px;
+                margin-top: 4px;
+            }
+
+            > p {
+                font-size: $font-15px;
+                color: $secondary-fg-color;
+                margin: 0;
+            }
+        }
+
+        .mx_SpaceCreateMenuType {
+            position: relative;
+            padding: 16px 32px 16px 72px;
+            width: 432px;
+            box-sizing: border-box;
+            border-radius: 8px;
+            border: 1px solid $input-darker-bg-color;
+            font-size: $font-15px;
+            margin: 20px 0;
+
+            > h3 {
+                font-weight: $font-semi-bold;
+                margin: 0 0 4px;
+            }
+
+            > span {
+                color: $secondary-fg-color;
+            }
+
+            &::before {
+                position: absolute;
+                content: '';
+                width: 32px;
+                height: 32px;
+                top: 24px;
+                left: 20px;
+                mask-position: center;
+                mask-repeat: no-repeat;
+                mask-size: 32px;
+                background-color: $tertiary-fg-color;
+            }
+
+            &:hover {
+                border-color: $accent-color;
+
+                &::before {
+                    background-color: $accent-color;
+                }
+
+                > span {
+                    color: $primary-fg-color;
+                }
+            }
+        }
+
+        .mx_SpaceCreateMenuType_public::before {
+            mask-image: url('$(res)/img/globe.svg');
+            mask-size: 26px;
+        }
+        .mx_SpaceCreateMenuType_private::before {
+            mask-image: url('$(res)/img/element-icons/lock.svg');
+        }
+
+        .mx_SpaceCreateMenu_back {
+            width: 28px;
+            height: 28px;
+            position: relative;
+            background-color: $theme-button-bg-color;
+            border-radius: 14px;
+            margin-bottom: 12px;
+
+            &::before {
+                content: "";
+                position: absolute;
+                height: 28px;
+                width: 28px;
+                top: 0;
+                left: 0;
+                background-color: $muted-fg-color;
+                transform: rotate(90deg);
+                mask-repeat: no-repeat;
+                mask-position: 2px 3px;
+                mask-size: 24px;
+                mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
+            }
+        }
+
+        .mx_FormButton {
+            padding: 8px 22px;
+            margin-left: auto;
+            display: block;
+            width: min-content;
+        }
+
+        .mx_AccessibleButton_disabled {
+            cursor: not-allowed;
+        }
+    }
+}
diff --git a/res/img/element-icons/lock.svg b/res/img/element-icons/lock.svg
new file mode 100644
index 0000000000..06fe52a391
--- /dev/null
+++ b/res/img/element-icons/lock.svg
@@ -0,0 +1,3 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1113 2.6665C11.1839 2.6665 8.00016 5.85026 8.00016 9.77762V13.3332L7.3335 13.3332C6.22893 13.3332 5.3335 14.2286 5.3335 15.3332V27.3332C5.3335 28.4377 6.22893 29.3332 7.3335 29.3332H24.6668C25.7714 29.3332 26.6668 28.4377 26.6668 27.3332V15.3332C26.6668 14.2286 25.7714 13.3332 24.6668 13.3332L24.0002 13.3332V9.77762C24.0002 5.85026 20.8164 2.6665 16.8891 2.6665H15.1113ZM20.4446 13.3332V9.77762C20.4446 7.81394 18.8527 6.22206 16.8891 6.22206H15.1113C13.1476 6.22206 11.5557 7.81394 11.5557 9.77762V13.3332H20.4446Z" fill="#8E99A4"/>
+</svg>
diff --git a/res/img/element-icons/plus.svg b/res/img/element-icons/plus.svg
new file mode 100644
index 0000000000..ea1972237d
--- /dev/null
+++ b/res/img/element-icons/plus.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path fill-rule="evenodd" clip-rule="evenodd" d="M8.74986 3.55554C8.74986 3.14133 8.41408 2.80554 7.99986 2.80554C7.58565 2.80554 7.24986 3.14133 7.24986 3.55554V7.24999L3.55542 7.24999C3.14121 7.24999 2.80542 7.58577 2.80542 7.99999C2.80542 8.4142 3.14121 8.74999 3.55542 8.74999L7.24987 8.74999V12.4444C7.24987 12.8586 7.58565 13.1944 7.99987 13.1944C8.41408 13.1944 8.74987 12.8586 8.74987 12.4444V8.74999L12.4443 8.74999C12.8585 8.74999 13.1943 8.4142 13.1943 7.99999C13.1943 7.58577 12.8585 7.24999 12.4443 7.24999L8.74986 7.24999V3.55554Z" fill="#8E99A4"/>
+</svg>
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index b5e5966d91..726ff547ff 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -76,6 +76,7 @@ export interface IProps extends IPosition {
     hasBackground?: boolean;
     // whether this context menu should be focus managed. If false it must handle itself
     managed?: boolean;
+    wrapperClassName?: string;
 
     // Function to be called on menu close
     onFinished();
@@ -365,7 +366,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
 
         return (
             <div
-                className="mx_ContextualMenu_wrapper"
+                className={classNames("mx_ContextualMenu_wrapper", this.props.wrapperClassName)}
                 style={{...position, ...wrapperStyle}}
                 onKeyDown={this.onKeyDown}
                 onContextMenu={this.onContextMenuPreventBubbling}
diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx
new file mode 100644
index 0000000000..bc378ab956
--- /dev/null
+++ b/src/components/views/spaces/SpaceBasicSettings.tsx
@@ -0,0 +1,120 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useRef, useState} from "react";
+
+import {_t} from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import Field from "../elements/Field";
+
+interface IProps {
+    avatarUrl?: string;
+    avatarDisabled?: boolean;
+    name?: string,
+    nameDisabled?: boolean;
+    topic?: string;
+    topicDisabled?: boolean;
+    setAvatar(avatar: File): void;
+    setName(name: string): void;
+    setTopic(topic: string): void;
+}
+
+const SpaceBasicSettings = ({
+    avatarUrl,
+    avatarDisabled = false,
+    setAvatar,
+    name = "",
+    nameDisabled = false,
+    setName,
+    topic = "",
+    topicDisabled = false,
+    setTopic,
+}: IProps) => {
+    const avatarUploadRef = useRef<HTMLInputElement>();
+    const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache
+
+    let avatarSection;
+    if (avatarDisabled) {
+        if (avatar) {
+            avatarSection = <img className="mx_SpaceBasicSettings_avatar" src={avatar} alt="" />;
+        } else {
+            avatarSection = <div className="mx_SpaceBasicSettings_avatar" />;
+        }
+    } else {
+        if (avatar) {
+            avatarSection = <React.Fragment>
+                <AccessibleButton
+                    className="mx_SpaceBasicSettings_avatar"
+                    onClick={() => avatarUploadRef.current?.click()}
+                    element="img"
+                    src={avatar}
+                    alt=""
+                />
+                <AccessibleButton onClick={() => {
+                    avatarUploadRef.current.value = "";
+                    setAvatarDataUrl(undefined);
+                    setAvatar(undefined);
+                }} kind="link" className="mx_SpaceBasicSettings_avatar_remove">
+                    { _t("Delete") }
+                </AccessibleButton>
+            </React.Fragment>;
+        } else {
+            avatarSection = <React.Fragment>
+                <div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} />
+                <AccessibleButton onClick={() => avatarUploadRef.current?.click()} kind="link">
+                    { _t("Upload") }
+                </AccessibleButton>
+            </React.Fragment>;
+        }
+    }
+
+    return <div className="mx_SpaceBasicSettings">
+        <div className="mx_SpaceBasicSettings_avatarContainer">
+            { avatarSection }
+            <input type="file" ref={avatarUploadRef} onChange={(e) => {
+                if (!e.target.files?.length) return;
+                const file = e.target.files[0];
+                setAvatar(file);
+                const reader = new FileReader();
+                reader.onload = (ev) => {
+                    setAvatarDataUrl(ev.target.result as string);
+                };
+                reader.readAsDataURL(file);
+            }} accept="image/*" />
+        </div>
+
+        <Field
+            name="spaceName"
+            label={_t("Name")}
+            autoFocus={true}
+            value={name}
+            onChange={ev => setName(ev.target.value)}
+            disabled={nameDisabled}
+        />
+
+        <Field
+            name="spaceTopic"
+            element="textarea"
+            label={_t("Description")}
+            value={topic}
+            onChange={ev => setTopic(ev.target.value)}
+            rows={3}
+            disabled={topicDisabled}
+        />
+    </div>;
+};
+
+export default SpaceBasicSettings;
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
new file mode 100644
index 0000000000..9d0543a6c5
--- /dev/null
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -0,0 +1,175 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useContext, useState} from "react";
+import classNames from "classnames";
+import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event";
+
+import {_t} from "../../../languageHandler";
+import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
+import FormButton from "../elements/FormButton";
+import createRoom, {IStateEvent, Preset} from "../../../createRoom";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import SpaceBasicSettings from "./SpaceBasicSettings";
+import AccessibleButton from "../elements/AccessibleButton";
+import FocusLock from "react-focus-lock";
+
+const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
+    return (
+        <AccessibleButton className={classNames("mx_SpaceCreateMenuType", className)} onClick={onClick}>
+            <h3>{ title }</h3>
+            <span>{ description }</span>
+        </AccessibleButton>
+    );
+};
+
+enum Visibility {
+    Public,
+    Private,
+}
+
+const SpaceCreateMenu = ({ onFinished }) => {
+    const cli = useContext(MatrixClientContext);
+    const [visibility, setVisibility] = useState<Visibility>(null);
+    const [name, setName] = useState("");
+    const [avatar, setAvatar] = useState<File>(null);
+    const [topic, setTopic] = useState<string>("");
+    const [busy, setBusy] = useState<boolean>(false);
+
+    const onSpaceCreateClick = async () => {
+        if (busy) return;
+        setBusy(true);
+        const initialState: IStateEvent[] = [
+            {
+                type: EventType.RoomHistoryVisibility,
+                content: {
+                    "history_visibility": visibility === Visibility.Public ? "world_readable" : "invited",
+                },
+            },
+        ];
+        if (avatar) {
+            const url = await cli.uploadContent(avatar);
+
+            initialState.push({
+                type: EventType.RoomAvatar,
+                content: { url },
+            });
+        }
+        if (topic) {
+            initialState.push({
+                type: EventType.RoomTopic,
+                content: { topic },
+            });
+        }
+
+        try {
+            await createRoom({
+                createOpts: {
+                    preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
+                    name,
+                    creation_content: {
+                        // Based on MSC1840
+                        [RoomCreateTypeField]: RoomType.Space,
+                    },
+                    initial_state: initialState,
+                    power_level_content_override: {
+                        // Only allow Admins to write to the timeline to prevent hidden sync spam
+                        events_default: 100,
+                    },
+                },
+                spinner: false,
+                encryption: false,
+                andView: true,
+                inlineErrors: true,
+            });
+
+            onFinished();
+        } catch (e) {
+            console.error(e);
+        }
+    };
+
+    let body;
+    if (visibility === null) {
+        body = <React.Fragment>
+            <h2>{ _t("Create a space") }</h2>
+            <p>{ _t("Organise rooms into spaces, for just you or anyone") }</p>
+
+            <SpaceCreateMenuType
+                title={_t("Public")}
+                description={_t("Open space for anyone, best for communities")}
+                className="mx_SpaceCreateMenuType_public"
+                onClick={() => setVisibility(Visibility.Public)}
+            />
+            <SpaceCreateMenuType
+                title={_t("Private")}
+                description={_t("Invite only space, best for yourself or teams")}
+                className="mx_SpaceCreateMenuType_private"
+                onClick={() => setVisibility(Visibility.Private)}
+            />
+
+            {/*<p>{ _t("Looking to join an existing space?") }</p>*/}
+        </React.Fragment>;
+    } else {
+        body = <React.Fragment>
+            <AccessibleTooltipButton
+                className="mx_SpaceCreateMenu_back"
+                onClick={() => setVisibility(null)}
+                title={_t("Go back")}
+            />
+
+            <h2>
+                {
+                    visibility === Visibility.Public
+                        ? _t("Personalise your public space")
+                        : _t("Personalise your private space")
+                }
+            </h2>
+            <p>
+                {
+                    _t("Give it a photo, name and description to help you identify it.")
+                } {
+                    _t("You can change these at any point.")
+                }
+            </p>
+
+            <SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} />
+
+            <FormButton
+                label={busy ? _t("Creating...") : _t("Create")}
+                onClick={onSpaceCreateClick}
+                disabled={!name && !busy}
+            />
+        </React.Fragment>;
+    }
+
+    return <ContextMenu
+        left={72}
+        top={62}
+        chevronOffset={0}
+        chevronFace={ChevronFace.None}
+        onFinished={onFinished}
+        wrapperClassName="mx_SpaceCreateMenu_wrapper"
+        managed={false}
+    >
+        <FocusLock returnFocus={true}>
+            { body }
+        </FocusLock>
+    </ContextMenu>;
+}
+
+export default SpaceCreateMenu;
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index 760181e0e0..48e2c86b2c 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -20,6 +20,8 @@ import {Room} from "matrix-js-sdk/src/models/room";
 
 import {_t} from "../../../languageHandler";
 import RoomAvatar from "../avatars/RoomAvatar";
+import {useContextMenu} from "../../structures/ContextMenu";
+import SpaceCreateMenu from "./SpaceCreateMenu";
 import {SpaceItem} from "./SpaceTreeLevel";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import {useEventEmitter} from "../../../hooks/useEventEmitter";
@@ -112,9 +114,21 @@ const useSpaces = (): [Room[], Room | null] => {
 };
 
 const SpacePanel = () => {
+    // We don't need the handle as we position the menu in a constant location
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
     const [spaces, activeSpace] = useSpaces();
     const [isPanelCollapsed, setPanelCollapsed] = useState(true);
 
+    const newClasses = classNames("mx_SpaceButton_new", {
+        mx_SpaceButton_newCancel: menuDisplayed,
+    });
+
+    let contextMenu = null;
+    if (menuDisplayed) {
+        contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
+    }
+
     const onKeyDown = (ev: React.KeyboardEvent) => {
         let handled = true;
 
@@ -203,12 +217,19 @@ const SpacePanel = () => {
                             onExpand={() => setPanelCollapsed(false)}
                         />) }
                     </div>
+                    <SpaceButton
+                        className={newClasses}
+                        tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
+                        onClick={menuDisplayed ? closeMenu : openMenu}
+                        isNarrow={isPanelCollapsed}
+                    />
                 </AutoHideScrollbar>
                 <AccessibleTooltipButton
                     className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
                     onClick={evt => setPanelCollapsed(!isPanelCollapsed)}
                     title={expandCollapseButtonTitle}
                 />
+                { contextMenu }
             </ul>
         )}
     </RovingTabIndexProvider>
diff --git a/src/createRoom.ts b/src/createRoom.ts
index 9e3960cdb7..e773c51290 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -41,7 +41,7 @@ enum Visibility {
     Private = "private",
 }
 
-enum Preset {
+export enum Preset {
     PrivateChat = "private_chat",
     TrustedPrivateChat = "trusted_private_chat",
     PublicChat = "public_chat",
@@ -54,7 +54,7 @@ interface Invite3PID {
     address: string;
 }
 
-interface IStateEvent {
+export interface IStateEvent {
     type: string;
     state_key?: string; // defaults to an empty string
     content: object;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 38460a5f6e..1b29e65b40 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -978,11 +978,27 @@
     "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
     "Decline (%(counter)s)": "Decline (%(counter)s)",
     "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
+    "Delete": "Delete",
+    "Upload": "Upload",
+    "Name": "Name",
+    "Description": "Description",
+    "Create a space": "Create a space",
+    "Organise rooms into spaces, for just you or anyone": "Organise rooms into spaces, for just you or anyone",
+    "Public": "Public",
+    "Open space for anyone, best for communities": "Open space for anyone, best for communities",
+    "Private": "Private",
+    "Invite only space, best for yourself or teams": "Invite only space, best for yourself or teams",
+    "Go back": "Go back",
+    "Personalise your public space": "Personalise your public space",
+    "Personalise your private space": "Personalise your private space",
+    "Give it a photo, name and description to help you identify it.": "Give it a photo, name and description to help you identify it.",
+    "You can change these at any point.": "You can change these at any point.",
+    "Creating...": "Creating...",
+    "Create": "Create",
     "Expand space panel": "Expand space panel",
     "Collapse space panel": "Collapse space panel",
     "Home": "Home",
     "Remove": "Remove",
-    "Upload": "Upload",
     "This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
     "This bridge is managed by <user />.": "This bridge is managed by <user />.",
     "Workspace: <networkLink/>": "Workspace: <networkLink/>",
@@ -1136,7 +1152,6 @@
     "Disconnect anyway": "Disconnect anyway",
     "You are still <b>sharing your personal data</b> on the identity server <idserver />.": "You are still <b>sharing your personal data</b> on the identity server <idserver />.",
     "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.",
-    "Go back": "Go back",
     "Identity Server (%(server)s)": "Identity Server (%(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.": "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.",
     "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.",
@@ -2011,7 +2026,6 @@
     "You can change this later if needed.": "You can change this later if needed.",
     "What's the name of your community or team?": "What's the name of your community or team?",
     "Enter name": "Enter name",
-    "Create": "Create",
     "Add image (optional)": "Add image (optional)",
     "An image will help people identify your community.": "An image will help people identify your community.",
     "Community IDs cannot be empty.": "Community IDs cannot be empty.",
@@ -2033,7 +2047,6 @@
     "Create a public room": "Create a public room",
     "Create a private room": "Create a private room",
     "Create a room in %(communityName)s": "Create a room in %(communityName)s",
-    "Name": "Name",
     "Topic (optional)": "Topic (optional)",
     "Make this room public": "Make this room public",
     "Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
@@ -2456,7 +2469,6 @@
     "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",
     "Failed to load %(groupId)s": "Failed to load %(groupId)s",

From c8fe3f76765108869e85a4b0140461210c0f5eff Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 1 Mar 2021 17:54:53 +0000
Subject: [PATCH 235/389] Pass room creation opts for new rooms into RoomView

---
 src/components/structures/LoggedInView.tsx | 3 +++
 src/components/structures/MatrixChat.tsx   | 6 +++++-
 src/components/structures/RoomView.tsx     | 3 +++
 src/createRoom.ts                          | 6 +++++-
 4 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index c01214f3f4..1694b4bcf5 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -55,6 +55,7 @@ import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
 import Modal from "../../Modal";
 import { ICollapseConfig } from "../../resizer/distributors/collapse";
 import HostSignupContainer from '../views/host_signup/HostSignupContainer';
+import { IOpts } from "../../createRoom";
 
 // We need to fetch each pinned message individually (if we don't already have it)
 // so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -91,6 +92,7 @@ interface IProps {
     currentGroupId?: string;
     currentGroupIsNew?: boolean;
     justRegistered?: boolean;
+    roomJustCreatedOpts?: IOpts;
 }
 
 interface IUsageLimit {
@@ -619,6 +621,7 @@ class LoggedInView extends React.Component<IProps, IState> {
                     viaServers={this.props.viaServers}
                     key={this.props.currentRoomId || 'roomview'}
                     resizeNotifier={this.props.resizeNotifier}
+                    justCreatedOpts={this.props.roomJustCreatedOpts}
                 />;
                 break;
 
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 5045e44182..8e3d3e6b5f 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -48,7 +48,7 @@ import * as Lifecycle from '../../Lifecycle';
 import '../../stores/LifecycleStore';
 import PageTypes from '../../PageTypes';
 
-import createRoom from "../../createRoom";
+import createRoom, {IOpts} from "../../createRoom";
 import {_t, _td, getCurrentLanguage} from '../../languageHandler';
 import SettingsStore from "../../settings/SettingsStore";
 import ThemeController from "../../settings/controllers/ThemeController";
@@ -144,6 +144,8 @@ interface IRoomInfo {
     oob_data?: object;
     via_servers?: string[];
     threepid_invite?: IThreepidInvite;
+
+    justCreatedOpts?: IOpts;
 }
 /* eslint-enable camelcase */
 
@@ -201,6 +203,7 @@ interface IState {
     viaServers?: string[];
     pendingInitialSync?: boolean;
     justRegistered?: boolean;
+    roomJustCreatedOpts?: IOpts;
 }
 
 export default class MatrixChat extends React.PureComponent<IProps, IState> {
@@ -922,6 +925,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                 roomOobData: roomInfo.oob_data,
                 viaServers: roomInfo.via_servers,
                 ready: true,
+                roomJustCreatedOpts: roomInfo.justCreatedOpts,
             }, () => {
                 this.notifyNewScreen('room/' + presentedId, replaceLast);
             });
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 6c8560f42c..933514754c 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -80,6 +80,8 @@ import { showToast as showNotificationsToast } from "../../toasts/DesktopNotific
 import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
 import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
 import { objectHasDiff } from "../../utils/objects";
+import SpaceRoomView from "./SpaceRoomView";
+import { IOpts } from "../../createRoom";
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -114,6 +116,7 @@ interface IProps {
 
     autoJoin?: boolean;
     resizeNotifier: ResizeNotifier;
+    justCreatedOpts?: IOpts;
 
     // Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU)
     onRegistered?(credentials: IMatrixClientCreds): void;
diff --git a/src/createRoom.ts b/src/createRoom.ts
index e773c51290..00a970eedc 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -75,7 +75,7 @@ interface ICreateOpts {
     power_level_content_override?: object;
 }
 
-interface IOpts {
+export interface IOpts {
     dmUserId?: string;
     createOpts?: ICreateOpts;
     spinner?: boolean;
@@ -197,6 +197,9 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
         // room has been created, so we race here with the client knowing that
         // the room exists, causing things like
         // https://github.com/vector-im/vector-web/issues/1813
+        // Even if we were to block on the echo, servers tend to split the room
+        // state over multiple syncs so we can't atomically know when we have the
+        // entire thing.
         if (opts.andView) {
             dis.dispatch({
                 action: 'view_room',
@@ -206,6 +209,7 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
                 // so we are expecting the room to come down the sync
                 // stream, if it hasn't already.
                 joining: true,
+                justCreatedOpts: opts,
             });
         }
         CountlyAnalytics.instance.trackRoomCreate(startTime, roomId);

From c10512fd569021089c34a8ea2b6083d0996f02b3 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 1 Mar 2021 18:10:17 +0000
Subject: [PATCH 236/389] Initial SpaceRoomView work

---
 res/css/_components.scss                      |   2 +
 res/css/structures/_SpaceRoomView.scss        | 244 +++++++++
 res/css/views/spaces/_SpacePublicShare.scss   |  60 +++
 res/img/element-icons/link.svg                |   3 +
 res/themes/dark/css/_dark.scss                |   1 +
 res/themes/legacy-dark/css/_legacy-dark.scss  |   1 +
 .../legacy-light/css/_legacy-light.scss       |   1 +
 res/themes/light/css/_light.scss              |   1 +
 src/RoomInvite.js                             |   9 +-
 src/components/structures/RightPanel.js       |  23 +-
 src/components/structures/RoomView.tsx        |  20 +-
 src/components/structures/SpaceRoomView.tsx   | 503 ++++++++++++++++++
 src/components/views/dialogs/InviteDialog.tsx |  66 ++-
 .../views/spaces/SpacePublicShare.tsx         |  65 +++
 .../payloads/SetRightPanelPhasePayload.ts     |   2 +
 src/hooks/useStateArray.ts                    |  29 +
 src/i18n/strings/en_EN.json                   |  41 +-
 src/stores/RightPanelStorePhases.ts           |  12 +
 src/utils/space.ts                            |  28 +
 19 files changed, 1066 insertions(+), 45 deletions(-)
 create mode 100644 res/css/structures/_SpaceRoomView.scss
 create mode 100644 res/css/views/spaces/_SpacePublicShare.scss
 create mode 100644 res/img/element-icons/link.svg
 create mode 100644 src/components/structures/SpaceRoomView.tsx
 create mode 100644 src/components/views/spaces/SpacePublicShare.tsx
 create mode 100644 src/hooks/useStateArray.ts
 create mode 100644 src/utils/space.ts

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 8d6597aefa..ca66aa60ec 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -28,6 +28,7 @@
 @import "./structures/_ScrollPanel.scss";
 @import "./structures/_SearchBox.scss";
 @import "./structures/_SpacePanel.scss";
+@import "./structures/_SpaceRoomView.scss";
 @import "./structures/_TabbedView.scss";
 @import "./structures/_ToastContainer.scss";
 @import "./structures/_UploadBar.scss";
@@ -235,6 +236,7 @@
 @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
 @import "./views/spaces/_SpaceBasicSettings.scss";
 @import "./views/spaces/_SpaceCreateMenu.scss";
+@import "./views/spaces/_SpacePublicShare.scss";
 @import "./views/terms/_InlineTermsAgreement.scss";
 @import "./views/toasts/_AnalyticsToast.scss";
 @import "./views/toasts/_NonUrgentEchoFailureToast.scss";
diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
new file mode 100644
index 0000000000..559f405e59
--- /dev/null
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -0,0 +1,244 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+$SpaceRoomViewInnerWidth: 428px;
+
+.mx_SpaceRoomView {
+    .mx_MainSplit > div:first-child {
+        padding: 80px 60px;
+        flex-grow: 1;
+
+        h1 {
+            margin: 0;
+            font-size: $font-24px;
+            font-weight: $font-semi-bold;
+            color: $primary-fg-color;
+            width: max-content;
+        }
+
+        .mx_SpaceRoomView_description {
+            font-size: $font-15px;
+            color: $secondary-fg-color;
+            margin-top: 12px;
+            margin-bottom: 24px;
+        }
+
+        .mx_SpaceRoomView_buttons {
+            display: block;
+            margin-top: 44px;
+            width: $SpaceRoomViewInnerWidth;
+            text-align: right; // button alignment right
+
+            .mx_FormButton {
+                padding: 8px 22px;
+                margin-left: 16px;
+            }
+        }
+
+        .mx_Field {
+            max-width: $SpaceRoomViewInnerWidth;
+
+            & + .mx_Field {
+                margin-top: 28px;
+            }
+        }
+
+        .mx_SpaceRoomView_errorText {
+            font-weight: $font-semi-bold;
+            font-size: $font-12px;
+            line-height: $font-15px;
+            color: $notice-primary-color;
+            margin-bottom: 28px;
+        }
+
+        .mx_AccessibleButton_disabled {
+            cursor: not-allowed;
+        }
+    }
+
+    .mx_SpaceRoomView_landing {
+        overflow-y: auto;
+
+        > .mx_BaseAvatar_image,
+        > .mx_BaseAvatar > .mx_BaseAvatar_image {
+            border-radius: 12px;
+        }
+
+        .mx_SpaceRoomView_landing_name {
+            margin: 24px 0 16px;
+            font-size: $font-15px;
+            color: $secondary-fg-color;
+
+            > span {
+                display: inline-block;
+            }
+
+            .mx_SpaceRoomView_landing_nameRow {
+                margin-top: 12px;
+
+                > h1 {
+                    display: inline-block;
+                }
+            }
+
+            .mx_SpaceRoomView_landing_inviter {
+                .mx_BaseAvatar {
+                    margin-right: 4px;
+                    vertical-align: middle;
+                }
+            }
+
+            .mx_SpaceRoomView_landing_memberCount {
+                position: relative;
+                margin-left: 24px;
+                padding: 0 0 0 28px;
+                line-height: $font-24px;
+                vertical-align: text-bottom;
+
+                &::before {
+                    position: absolute;
+                    content: '';
+                    width: 24px;
+                    height: 24px;
+                    top: 0;
+                    left: 0;
+                    mask-position: center;
+                    mask-repeat: no-repeat;
+                    mask-size: contain;
+                    background-color: $accent-color;
+                    mask-image: url('$(res)/img/element-icons/community-members.svg');
+                }
+            }
+        }
+
+        .mx_SpaceRoomView_landing_topic {
+            font-size: $font-15px;
+        }
+
+        .mx_SpaceRoomView_landing_joinButtons {
+            margin-top: 24px;
+
+            .mx_FormButton {
+                padding: 8px 22px;
+            }
+        }
+    }
+
+    .mx_SpaceRoomView_privateScope {
+        .mx_RadioButton {
+            width: $SpaceRoomViewInnerWidth;
+            border-radius: 8px;
+            border: 1px solid $space-button-outline-color;
+            padding: 16px 16px 16px 72px;
+            margin-top: 36px;
+            cursor: pointer;
+            box-sizing: border-box;
+            position: relative;
+
+            > div:first-of-type {
+                // hide radio dot
+                display: none;
+            }
+
+            .mx_RadioButton_content {
+                margin: 0;
+
+                > h3 {
+                    margin: 0 0 4px;
+                    font-size: $font-15px;
+                    font-weight: $font-semi-bold;
+                    line-height: $font-18px;
+                }
+
+                > div {
+                    color: $secondary-fg-color;
+                    font-size: $font-15px;
+                    line-height: $font-24px;
+                }
+            }
+
+            &::before {
+                content: "";
+                position: absolute;
+                height: 32px;
+                width: 32px;
+                top: 24px;
+                left: 20px;
+                background-color: $secondary-fg-color;
+                mask-repeat: no-repeat;
+                mask-position: center;
+                mask-size: contain;
+            }
+        }
+
+        .mx_RadioButton_checked {
+            border-color: $accent-color;
+
+            .mx_RadioButton_content {
+                > div {
+                    color: $primary-fg-color;
+                }
+            }
+
+            &::before {
+                background-color: $accent-color;
+            }
+        }
+
+        .mx_SpaceRoomView_privateScope_justMeButton::before {
+            mask-image: url('$(res)/img/element-icons/room/members.svg');
+        }
+
+        .mx_SpaceRoomView_privateScope_meAndMyTeammatesButton::before {
+            mask-image: url('$(res)/img/element-icons/community-members.svg');
+        }
+    }
+
+    .mx_SpaceRoomView_inviteTeammates {
+        .mx_SpaceRoomView_inviteTeammates_buttons {
+            color: $secondary-fg-color;
+            margin-top: 28px;
+
+            .mx_AccessibleButton {
+                position: relative;
+                display: inline-block;
+                padding-left: 32px;
+                line-height: 24px; // to center icons
+
+                &::before {
+                    content: "";
+                    position: absolute;
+                    height: 24px;
+                    width: 24px;
+                    top: 0;
+                    left: 0;
+                    background-color: $secondary-fg-color;
+                    mask-repeat: no-repeat;
+                    mask-position: center;
+                    mask-size: contain;
+                }
+
+                & + .mx_AccessibleButton {
+                    margin-left: 32px;
+                }
+            }
+
+            .mx_SpaceRoomView_inviteTeammates_inviteDialogButton::before {
+                mask-image: url('$(res)/img/element-icons/room/invite.svg');
+            }
+        }
+    }
+}
diff --git a/res/css/views/spaces/_SpacePublicShare.scss b/res/css/views/spaces/_SpacePublicShare.scss
new file mode 100644
index 0000000000..9ba0549ae3
--- /dev/null
+++ b/res/css/views/spaces/_SpacePublicShare.scss
@@ -0,0 +1,60 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_SpacePublicShare {
+    .mx_AccessibleButton {
+        border: 1px solid $space-button-outline-color;
+        box-sizing: border-box;
+        border-radius: 8px;
+        padding: 12px 24px 12px 52px;
+        margin-top: 16px;
+        width: $SpaceRoomViewInnerWidth;
+        font-size: $font-15px;
+        line-height: $font-24px;
+        position: relative;
+        display: flex;
+
+        > span {
+            color: #368bd6;
+            margin-left: auto;
+        }
+
+        &:hover {
+            background-color: rgba(141, 151, 165, 0.1);
+        }
+
+        &::before {
+            content: "";
+            position: absolute;
+            width: 30px;
+            height: 30px;
+            mask-repeat: no-repeat;
+            mask-size: contain;
+            mask-position: center;
+            background: $muted-fg-color;
+            left: 12px;
+            top: 9px;
+        }
+
+        &.mx_SpacePublicShare_shareButton::before {
+            mask-image: url('$(res)/img/element-icons/link.svg');
+        }
+
+        &.mx_SpacePublicShare_inviteButton::before {
+            mask-image: url('$(res)/img/element-icons/room/invite.svg');
+        }
+    }
+}
diff --git a/res/img/element-icons/link.svg b/res/img/element-icons/link.svg
new file mode 100644
index 0000000000..ab3d54b838
--- /dev/null
+++ b/res/img/element-icons/link.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 d="M12.5285 6.54089L13.0273 6.04207C14.4052 4.66426 16.6259 4.65104 17.9874 6.01253C19.349 7.37402 19.3357 9.59466 17.9579 10.9725L15.5878 13.3425C14.21 14.7203 11.9893 14.7335 10.6277 13.372M11.4717 17.4589L10.9727 17.9579C9.59481 19.3357 7.37409 19.349 6.01256 17.9875C4.65102 16.626 4.66426 14.4053 6.04211 13.0275L8.41203 10.6577C9.78988 9.27988 12.0106 9.26665 13.3721 10.6281" stroke="black" stroke-width="2" stroke-linecap="round"/>
+</svg>
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index a878aa3cdd..0de5e69782 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -123,6 +123,7 @@ $roomsublist-divider-color: $primary-fg-color;
 $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
 
 $groupFilterPanel-divider-color: $roomlist-header-color;
+$space-button-outline-color: rgba(141, 151, 165, 0.2);
 
 $roomtile-preview-color: $secondary-fg-color;
 $roomtile-default-badge-bg-color: #61708b;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 3e3c299af9..8c5f20178b 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -120,6 +120,7 @@ $roomsublist-divider-color: $primary-fg-color;
 $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
 
 $groupFilterPanel-divider-color: $roomlist-header-color;
+$space-button-outline-color: rgba(141, 151, 165, 0.2);
 
 $roomtile-preview-color: #9e9e9e;
 $roomtile-default-badge-bg-color: #61708b;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index a740ba155c..3ba10a68ea 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -187,6 +187,7 @@ $roomsublist-divider-color: $primary-fg-color;
 $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
 
 $groupFilterPanel-divider-color: $roomlist-header-color;
+$space-button-outline-color: #E3E8F0;
 
 $roomtile-preview-color: #9e9e9e;
 $roomtile-default-badge-bg-color: #61708b;
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 1c89d83c01..76bf2ddc21 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -181,6 +181,7 @@ $roomsublist-divider-color: $primary-fg-color;
 $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
 
 $groupFilterPanel-divider-color: $roomlist-header-color;
+$space-button-outline-color: #E3E8F0;
 
 $roomtile-preview-color: $secondary-fg-color;
 $roomtile-default-badge-bg-color: #61708b;
diff --git a/src/RoomInvite.js b/src/RoomInvite.js
index 06d3fb04e8..728ae11e79 100644
--- a/src/RoomInvite.js
+++ b/src/RoomInvite.js
@@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter';
 import Modal from './Modal';
 import * as sdk from './';
 import { _t } from './languageHandler';
-import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
+import InviteDialog, {KIND_DM, KIND_INVITE, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog";
 import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
 import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
 
@@ -75,6 +75,13 @@ export function showCommunityInviteDialog(communityId) {
     }
 }
 
+export const showSpaceInviteDialog = (roomId) => {
+    Modal.createTrackedDialog("Invite Users", "Space", InviteDialog, {
+        kind: KIND_SPACE_INVITE,
+        roomId,
+    }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
+};
+
 /**
  * Checks if the given MatrixEvent is a valid 3rd party user invite.
  * @param {MatrixEvent} event The event to check
diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index d66049d3a5..3d9df2e927 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -24,7 +24,11 @@ import dis from '../../dispatcher/dispatcher';
 import RateLimitedFunc from '../../ratelimitedfunc';
 import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
 import GroupStore from '../../stores/GroupStore';
-import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
+import {
+    RightPanelPhases,
+    RIGHT_PANEL_PHASES_NO_ARGS,
+    RIGHT_PANEL_SPACE_PHASES,
+} from "../../stores/RightPanelStorePhases";
 import RightPanelStore from "../../stores/RightPanelStore";
 import MatrixClientContext from "../../contexts/MatrixClientContext";
 import {Action} from "../../dispatcher/actions";
@@ -79,6 +83,8 @@ export default class RightPanel extends React.Component {
                 return RightPanelPhases.GroupMemberList;
             }
             return rps.groupPanelPhase;
+        } else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) {
+            return RightPanelPhases.SpaceMemberList;
         } else if (userForPanel) {
             // XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
             // from its props and some from a store, except if the contents of the store changes
@@ -99,9 +105,8 @@ export default class RightPanel extends React.Component {
                 return rps.roomPanelPhase;
             }
             return RightPanelPhases.RoomMemberInfo;
-        } else {
-            return rps.roomPanelPhase;
         }
+        return rps.roomPanelPhase;
     }
 
     componentDidMount() {
@@ -181,6 +186,7 @@ export default class RightPanel extends React.Component {
                 verificationRequest: payload.verificationRequest,
                 verificationRequestPromise: payload.verificationRequestPromise,
                 widgetId: payload.widgetId,
+                space: payload.space,
             });
         }
     }
@@ -232,6 +238,13 @@ export default class RightPanel extends React.Component {
                     panel = <MemberList roomId={roomId} key={roomId} onClose={this.onClose} />;
                 }
                 break;
+            case RightPanelPhases.SpaceMemberList:
+                panel = <MemberList
+                    roomId={this.state.space ? this.state.space.roomId : roomId}
+                    key={this.state.space ? this.state.space.roomId : roomId}
+                    onClose={this.onClose}
+                />;
+                break;
 
             case RightPanelPhases.GroupMemberList:
                 if (this.props.groupId) {
@@ -244,10 +257,11 @@ export default class RightPanel extends React.Component {
                 break;
 
             case RightPanelPhases.RoomMemberInfo:
+            case RightPanelPhases.SpaceMemberInfo:
             case RightPanelPhases.EncryptionPanel:
                 panel = <UserInfo
                     user={this.state.member}
-                    room={this.props.room}
+                    room={this.state.phase === RightPanelPhases.SpaceMemberInfo ? this.state.space : this.props.room}
                     key={roomId || this.state.member.userId}
                     onClose={this.onClose}
                     phase={this.state.phase}
@@ -257,6 +271,7 @@ export default class RightPanel extends React.Component {
                 break;
 
             case RightPanelPhases.Room3pidMemberInfo:
+            case RightPanelPhases.Space3pidMemberInfo:
                 panel = <ThirdPartyMemberInfo event={this.state.event} key={roomId} />;
                 break;
 
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 933514754c..1961779d0e 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1400,7 +1400,7 @@ export default class RoomView extends React.Component<IProps, IState> {
         });
     };
 
-    private onRejectButtonClicked = ev => {
+    private onRejectButtonClicked = () => {
         this.setState({
             rejecting: true,
         });
@@ -1460,7 +1460,7 @@ export default class RoomView extends React.Component<IProps, IState> {
         }
     };
 
-    private onRejectThreepidInviteButtonClicked = ev => {
+    private onRejectThreepidInviteButtonClicked = () => {
         // We can reject 3pid invites in the same way that we accept them,
         // using /leave rather than /join. In the short term though, we
         // just ignore them.
@@ -1723,7 +1723,7 @@ export default class RoomView extends React.Component<IProps, IState> {
         }
 
         const myMembership = this.state.room.getMyMembership();
-        if (myMembership == 'invite') {
+        if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself
             if (this.state.joining || this.state.rejecting) {
                 return (
                     <ErrorBoundary>
@@ -1852,7 +1852,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                     room={this.state.room}
                 />
             );
-            if (!this.state.canPeek) {
+            if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
                 return (
                     <div className="mx_RoomView">
                         { previewBar }
@@ -1874,6 +1874,18 @@ export default class RoomView extends React.Component<IProps, IState> {
             );
         }
 
+        if (this.state.room?.isSpaceRoom()) {
+            return <SpaceRoomView
+                space={this.state.room}
+                justCreatedOpts={this.props.justCreatedOpts}
+                resizeNotifier={this.props.resizeNotifier}
+                onJoinButtonClicked={this.onJoinButtonClicked}
+                onRejectButtonClicked={this.props.threepidInvite
+                    ? this.onRejectThreepidInviteButtonClicked
+                    : this.onRejectButtonClicked}
+            />;
+        }
+
         const auxPanel = (
             <AuxPanel
                 room={this.state.room}
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
new file mode 100644
index 0000000000..6c64df31eb
--- /dev/null
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -0,0 +1,503 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {RefObject, useContext, useRef, useState} from "react";
+import {EventType} from "matrix-js-sdk/src/@types/event";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import MatrixClientContext from "../../contexts/MatrixClientContext";
+import RoomAvatar from "../views/avatars/RoomAvatar";
+import {_t} from "../../languageHandler";
+import AccessibleButton from "../views/elements/AccessibleButton";
+import RoomName from "../views/elements/RoomName";
+import RoomTopic from "../views/elements/RoomTopic";
+import FormButton from "../views/elements/FormButton";
+import {inviteMultipleToRoom, showSpaceInviteDialog} from "../../RoomInvite";
+import {useRoomMembers} from "../../hooks/useRoomMembers";
+import createRoom, {IOpts, Preset} from "../../createRoom";
+import Field from "../views/elements/Field";
+import {useEventEmitter} from "../../hooks/useEventEmitter";
+import StyledRadioGroup from "../views/elements/StyledRadioGroup";
+import withValidation from "../views/elements/Validation";
+import * as Email from "../../email";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import {Action} from "../../dispatcher/actions";
+import ResizeNotifier from "../../utils/ResizeNotifier"
+import MainSplit from './MainSplit';
+import ErrorBoundary from "../views/elements/ErrorBoundary";
+import {ActionPayload} from "../../dispatcher/payloads";
+import RightPanel from "./RightPanel";
+import RightPanelStore from "../../stores/RightPanelStore";
+import {EventSubscription} from "fbemitter";
+import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
+import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
+import {useStateArray} from "../../hooks/useStateArray";
+import SpacePublicShare from "../views/spaces/SpacePublicShare";
+import {shouldShowSpaceSettings} from "../../utils/space";
+import MemberAvatar from "../views/avatars/MemberAvatar";
+
+interface IProps {
+    space: Room;
+    justCreatedOpts?: IOpts;
+    resizeNotifier: ResizeNotifier;
+    onJoinButtonClicked(): void;
+    onRejectButtonClicked(): void;
+}
+
+interface IState {
+    phase: Phase;
+    showRightPanel: boolean;
+}
+
+enum Phase {
+    Landing,
+    PublicCreateRooms,
+    PublicShare,
+    PrivateScope,
+    PrivateInvite,
+    PrivateCreateRooms,
+    PrivateExistingRooms,
+}
+
+const RoomMemberCount = ({ room, children }) => {
+    const members = useRoomMembers(room);
+    const count = members.length;
+
+    if (children) return children(count);
+    return count;
+};
+
+const useMyRoomMembership = (room: Room) => {
+    const [membership, setMembership] = useState(room.getMyMembership());
+    useEventEmitter(room, "Room.myMembership", () => {
+        setMembership(room.getMyMembership());
+    });
+    return membership;
+};
+
+const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
+    const cli = useContext(MatrixClientContext);
+    const myMembership = useMyRoomMembership(space);
+    const joinRule = space.getJoinRule();
+    const userId = cli.getUserId();
+
+    let joinButtons;
+    if (myMembership === "invite") {
+        joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
+            <FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} />
+            <AccessibleButton kind="link" onClick={onRejectButtonClicked}>
+                {_t("Decline")}
+            </AccessibleButton>
+        </div>;
+    } else if (myMembership !== "join" && joinRule === "public") {
+        joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
+            <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
+        </div>;
+    }
+
+    return <div className="mx_SpaceRoomView_landing">
+        <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
+        <div className="mx_SpaceRoomView_landing_name">
+            <RoomName room={space}>
+                {(name) => {
+                    const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
+                        <h1>{ name }</h1>
+                        <RoomMemberCount room={space}>
+                            {(count) => count > 0 ? (
+                                <AccessibleButton
+                                    className="mx_SpaceRoomView_landing_memberCount"
+                                    kind="link"
+                                    onClick={() => {
+                                        defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
+                                            action: Action.SetRightPanelPhase,
+                                            phase: RightPanelPhases.RoomMemberList,
+                                            refireParams: { space },
+                                        });
+                                    }}
+                                >
+                                    { _t("%(count)s members", { count }) }
+                                </AccessibleButton>
+                            ) : null}
+                        </RoomMemberCount>
+                    </div> };
+                    if (myMembership === "invite") {
+                        const inviteSender = space.getMember(userId)?.events.member?.getSender();
+                        const inviter = inviteSender && space.getMember(inviteSender);
+
+                        if (inviteSender) {
+                            return _t("<inviter/> invited you to <name/>", {}, {
+                                name: tags.name,
+                                inviter: () => inviter
+                                    ? <span className="mx_SpaceRoomView_landing_inviter">
+                                        <MemberAvatar member={inviter} width={26} height={26} viewUserOnClick={true} />
+                                        { inviter.name }
+                                    </span>
+                                    : <span className="mx_SpaceRoomView_landing_inviter">
+                                        { inviteSender }
+                                    </span>,
+                            }) as JSX.Element;
+                        } else {
+                            return _t("You have been invited to <name/>", {}, tags) as JSX.Element;
+                        }
+                    } else if (shouldShowSpaceSettings(cli, space)) {
+                        if (space.getJoinRule() === "public") {
+                            return _t("Your public space <name/>", {}, tags) as JSX.Element;
+                        } else {
+                            return _t("Your private space <name/>", {}, tags) as JSX.Element;
+                        }
+                    }
+                    return _t("Welcome to <name/>", {}, tags) as JSX.Element;
+                }}
+            </RoomName>
+        </div>
+        <div className="mx_SpaceRoomView_landing_topic">
+            <RoomTopic room={space} />
+        </div>
+        { joinButtons }
+    </div>;
+};
+
+const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
+    const [busy, setBusy] = useState(false);
+    const [error, setError] = useState("");
+    const numFields = 3;
+    const placeholders = [_t("General"), _t("Random"), _t("Support")];
+    // TODO vary default prefills for "Just Me" spaces
+    const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
+    const fields = new Array(numFields).fill(0).map((_, i) => {
+        const name = "roomName" + i;
+        return <Field
+            key={name}
+            name={name}
+            type="text"
+            label={_t("Room name")}
+            placeholder={placeholders[i]}
+            value={roomNames[i]}
+            onChange={ev => setRoomName(i, ev.target.value)}
+        />;
+    });
+
+    const onNextClick = async () => {
+        setError("");
+        setBusy(true);
+        try {
+            await Promise.all(roomNames.map(name => name.trim()).filter(Boolean).map(name => {
+                return createRoom({
+                    createOpts: {
+                        preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
+                        name,
+                    },
+                    spinner: false,
+                    encryption: false,
+                    andView: false,
+                    inlineErrors: true,
+                    parentSpace: space,
+                });
+            }));
+            onFinished();
+        } catch (e) {
+            console.error("Failed to create initial space rooms", e);
+            setError(_t("Failed to create initial space rooms"));
+        }
+        setBusy(false);
+    };
+
+    let onClick = onFinished;
+    let buttonLabel = _t("Skip for now");
+    if (roomNames.some(name => name.trim())) {
+        onClick = onNextClick;
+        buttonLabel = busy ? _t("Creating rooms...") : _t("Next")
+    }
+
+    return <div>
+        <h1>{ title }</h1>
+        <div className="mx_SpaceRoomView_description">{ description }</div>
+
+        { error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
+        { fields }
+
+        <div className="mx_SpaceRoomView_buttons">
+            <FormButton
+                label={buttonLabel}
+                disabled={busy}
+                onClick={onClick}
+            />
+        </div>
+    </div>;
+};
+
+const SpaceSetupPublicShare = ({ space, onFinished }) => {
+    return <div className="mx_SpaceRoomView_publicShare">
+        <h1>{ _t("Share your public space") }</h1>
+        <div className="mx_SpacePublicShare_description">{ _t("At the moment only you can see it.") }</div>
+
+        <SpacePublicShare space={space} onFinished={onFinished} />
+
+        <div className="mx_SpaceRoomView_buttons">
+            <FormButton label={_t("Finish")} onClick={onFinished} />
+        </div>
+    </div>;
+};
+
+const SpaceSetupPrivateScope = ({ onFinished }) => {
+    const [option, setOption] = useState<string>(null);
+
+    return <div className="mx_SpaceRoomView_privateScope">
+        <h1>{ _t("Who are you working with?") }</h1>
+        <div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
+
+        <StyledRadioGroup
+            name="privateSpaceScope"
+            value={option}
+            onChange={setOption}
+            definitions={[
+                {
+                    value: "justMe",
+                    className: "mx_SpaceRoomView_privateScope_justMeButton",
+                    label: <React.Fragment>
+                        <h3>{ _t("Just Me") }</h3>
+                        <div>{ _t("A private space just for you") }</div>
+                    </React.Fragment>,
+                }, {
+                    value: "meAndMyTeammates",
+                    className: "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton",
+                    label: <React.Fragment>
+                        <h3>{ _t("Me and my teammates") }</h3>
+                        <div>{ _t("A private space for you and your teammates") }</div>
+                    </React.Fragment>,
+                },
+            ]}
+        />
+
+        <div className="mx_SpaceRoomView_buttons">
+            <FormButton label={_t("Next")} disabled={!option} onClick={() => onFinished(option !== "justMe")} />
+        </div>
+    </div>;
+};
+
+const validateEmailRules = withValidation({
+    rules: [{
+        key: "email",
+        test: ({ value }) => !value || Email.looksValid(value),
+        invalid: () => _t("Doesn't look like a valid email address"),
+    }],
+});
+
+const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
+    const [busy, setBusy] = useState(false);
+    const [error, setError] = useState("");
+    const numFields = 3;
+    const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
+    const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
+    const fields = new Array(numFields).fill(0).map((_, i) => {
+        const name = "emailAddress" + i;
+        return <Field
+            key={name}
+            name={name}
+            type="text"
+            label={_t("Email address")}
+            placeholder={_t("Email")}
+            value={emailAddresses[i]}
+            onChange={ev => setEmailAddress(i, ev.target.value)}
+            ref={fieldRefs[i]}
+            onValidate={validateEmailRules}
+        />;
+    });
+
+    const onNextClick = async () => {
+        setError("");
+        for (let i = 0; i < fieldRefs.length; i++) {
+            const fieldRef = fieldRefs[i];
+            const valid = await fieldRef.current.validate({ allowEmpty: true });
+
+            if (valid === false) { // true/null are allowed
+                fieldRef.current.focus();
+                fieldRef.current.validate({ allowEmpty: true, focused: true });
+                return;
+            }
+        }
+
+        setBusy(true);
+        const targetIds = emailAddresses.map(name => name.trim()).filter(Boolean);
+        try {
+            const result = await inviteMultipleToRoom(space.roomId, targetIds);
+
+            const failedUsers = Object.keys(result.states).filter(a => result.states[a] === "error");
+            if (failedUsers.length > 0) {
+                console.log("Failed to invite users to space: ", result);
+                setError(_t("Failed to invite the following users to your space: %(csvUsers)s", {
+                    csvUsers: failedUsers.join(", "),
+                }));
+            } else {
+                onFinished();
+            }
+        } catch (err) {
+            console.error("Failed to invite users to space: ", err);
+            setError(_t("We couldn't invite those users. Please check the users you want to invite and try again."));
+        }
+        setBusy(false);
+    };
+
+    return <div className="mx_SpaceRoomView_inviteTeammates">
+        <h1>{ _t("Invite your teammates") }</h1>
+        <div className="mx_SpaceRoomView_description">{ _t("Ensure the right people have access to the space.") }</div>
+
+        { error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
+        { fields }
+
+        <div className="mx_SpaceRoomView_inviteTeammates_buttons">
+            <AccessibleButton
+                className="mx_SpaceRoomView_inviteTeammates_inviteDialogButton"
+                onClick={() => showSpaceInviteDialog(space.roomId)}
+            >
+                { _t("Invite by username") }
+            </AccessibleButton>
+        </div>
+
+        <div className="mx_SpaceRoomView_buttons">
+            <AccessibleButton onClick={onFinished} kind="link">{_t("Skip for now")}</AccessibleButton>
+            <FormButton label={busy ? _t("Inviting...") : _t("Next")} disabled={busy} onClick={onNextClick} />
+        </div>
+    </div>;
+};
+
+export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
+    static contextType = MatrixClientContext;
+
+    private readonly creator: string;
+    private readonly dispatcherRef: string;
+    private readonly rightPanelStoreToken: EventSubscription;
+
+    constructor(props, context) {
+        super(props, context);
+
+        let phase = Phase.Landing;
+
+        this.creator = this.props.space.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
+        const showSetup = this.props.justCreatedOpts && this.context.getUserId() === this.creator;
+
+        if (showSetup) {
+            phase = this.props.justCreatedOpts.createOpts.preset === Preset.PublicChat
+                ? Phase.PublicCreateRooms : Phase.PrivateScope;
+        }
+
+        this.state = {
+            phase,
+            showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
+        };
+
+        this.dispatcherRef = defaultDispatcher.register(this.onAction);
+        this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
+    }
+
+    componentWillUnmount() {
+        defaultDispatcher.unregister(this.dispatcherRef);
+        this.rightPanelStoreToken.remove();
+    }
+
+    private onRightPanelStoreUpdate = () => {
+        this.setState({
+            showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
+        });
+    };
+
+    private onAction = (payload: ActionPayload) => {
+        if (payload.action !== Action.ViewUser && payload.action !== "view_3pid_invite") return;
+
+        if (payload.action === Action.ViewUser && payload.member) {
+            defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
+                action: Action.SetRightPanelPhase,
+                phase: RightPanelPhases.SpaceMemberInfo,
+                refireParams: {
+                    space: this.props.space,
+                    member: payload.member,
+                },
+            });
+        } else if (payload.action === "view_3pid_invite" && payload.event) {
+            defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
+                action: Action.SetRightPanelPhase,
+                phase: RightPanelPhases.Space3pidMemberInfo,
+                refireParams: {
+                    space: this.props.space,
+                    event: payload.event,
+                },
+            });
+        } else {
+            defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
+                action: Action.SetRightPanelPhase,
+                phase: RightPanelPhases.SpaceMemberList,
+                refireParams: { space: this.props.space },
+            });
+        }
+    };
+
+    private renderBody() {
+        switch (this.state.phase) {
+            case Phase.Landing:
+                return <SpaceLanding
+                    space={this.props.space}
+                    onJoinButtonClicked={this.props.onJoinButtonClicked}
+                    onRejectButtonClicked={this.props.onRejectButtonClicked}
+                />;
+
+            case Phase.PublicCreateRooms:
+                return <SpaceSetupFirstRooms
+                    space={this.props.space}
+                    title={_t("What discussions do you want to have?")}
+                    description={_t("We'll create rooms for each topic.")}
+                    onFinished={() => this.setState({ phase: Phase.PublicShare })}
+                />;
+            case Phase.PublicShare:
+                return <SpaceSetupPublicShare
+                    space={this.props.space}
+                    onFinished={() => this.setState({ phase: Phase.Landing })}
+                />;
+
+            case Phase.PrivateScope:
+                return <SpaceSetupPrivateScope
+                    onFinished={(invite: boolean) => {
+                        this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms });
+                    }}
+                />;
+            case Phase.PrivateInvite:
+                return <SpaceSetupPrivateInvite
+                    space={this.props.space}
+                    onFinished={() => this.setState({ phase: Phase.PrivateCreateRooms })}
+                />;
+            case Phase.PrivateCreateRooms:
+                return <SpaceSetupFirstRooms
+                    space={this.props.space}
+                    title={_t("What projects are you working on?")}
+                    description={_t("We'll create rooms for each of them. You can add existing rooms after setup.")}
+                    onFinished={() => this.setState({ phase: Phase.Landing })}
+                />;
+        }
+    }
+
+    render() {
+        const rightPanel = this.state.showRightPanel && this.state.phase === Phase.Landing
+            ? <RightPanel room={this.props.space} resizeNotifier={this.props.resizeNotifier} />
+            : null;
+
+        return <main className="mx_SpaceRoomView">
+            <ErrorBoundary>
+                <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
+                    { this.renderBody() }
+                </MainSplit>
+            </ErrorBoundary>
+        </main>;
+    }
+}
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 5b936e822c..9bc5b6476f 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import React, {createRef} from 'react';
-import {_t} from "../../../languageHandler";
+import {_t, _td} from "../../../languageHandler";
 import * as sdk from "../../../index";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
@@ -48,6 +48,7 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 
 export const KIND_DM = "dm";
 export const KIND_INVITE = "invite";
+export const KIND_SPACE_INVITE = "space_invite";
 export const KIND_CALL_TRANSFER = "call_transfer";
 
 const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
@@ -309,7 +310,7 @@ interface IInviteDialogProps {
     // not provided.
     kind: string,
 
-    // The room ID this dialog is for. Only required for KIND_INVITE.
+    // The room ID this dialog is for. Only required for KIND_INVITE and KIND_SPACE_INVITE.
     roomId: string,
 
     // The call to transfer. Only required for KIND_CALL_TRANSFER.
@@ -348,8 +349,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
     constructor(props) {
         super(props);
 
-        if (props.kind === KIND_INVITE && !props.roomId) {
-            throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
+        if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) {
+            throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog");
         } else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
             throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
         }
@@ -1026,7 +1027,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             sectionSubname = _t("May include members not in %(communityName)s", {communityName});
         }
 
-        if (this.props.kind === KIND_INVITE) {
+        if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
             sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
         }
 
@@ -1247,38 +1248,35 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             }
             buttonText = _t("Go");
             goButtonFn = this._startDm;
-        } else if (this.props.kind === KIND_INVITE) {
-            title = _t("Invite to this room");
+        } else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
+            title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space");
 
-            if (identityServersEnabled) {
-                helpText = _t(
-                    "Invite someone using their name, email address, username (like <userId/>) or " +
-                        "<a>share this room</a>.",
-                    {},
-                    {
-                        userId: () =>
-                            <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
-                        a: (sub) =>
-                            <a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
-                                {sub}
-                            </a>,
-                    },
-                );
-            } else {
-                helpText = _t(
-                    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
-                    {},
-                    {
-                        userId: () =>
-                            <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
-                        a: (sub) =>
-                            <a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
-                                {sub}
-                            </a>,
-                    },
-                );
+            let helpTextUntranslated;
+            if (this.props.kind === KIND_INVITE) {
+                if (identityServersEnabled) {
+                    helpTextUntranslated = _td("Invite someone using their name, email address, username " +
+                        "(like <userId/>) or <a>share this room</a>.");
+                } else {
+                    helpTextUntranslated = _td("Invite someone using their name, username " +
+                        "(like <userId/>) or <a>share this room</a>.");
+                }
+            } else { // KIND_SPACE_INVITE
+                if (identityServersEnabled) {
+                    helpTextUntranslated = _td("Invite someone using their name, email address, username " +
+                        "(like <userId/>) or <a>share this space</a>.");
+                } else {
+                    helpTextUntranslated = _td("Invite someone using their name, username " +
+                        "(like <userId/>) or <a>share this space</a>.");
+                }
             }
 
+            helpText = _t(helpTextUntranslated, {}, {
+                userId: () =>
+                    <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
+                a: (sub) =>
+                    <a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
+            });
+
             buttonText = _t("Invite");
             goButtonFn = this._inviteUsers;
         } else if (this.props.kind === KIND_CALL_TRANSFER) {
diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx
new file mode 100644
index 0000000000..064d1640a2
--- /dev/null
+++ b/src/components/views/spaces/SpacePublicShare.tsx
@@ -0,0 +1,65 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useState} from "react";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import {_t} from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import {copyPlaintext} from "../../../utils/strings";
+import {sleep} from "../../../utils/promise";
+import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
+import {showSpaceInviteDialog} from "../../../RoomInvite";
+
+interface IProps {
+    space: Room;
+    onFinished(): void;
+}
+
+const SpacePublicShare = ({ space, onFinished }: IProps) => {
+    const [copiedText, setCopiedText] = useState(_t("Click to copy"));
+
+    return <div className="mx_SpacePublicShare">
+        <AccessibleButton
+            className="mx_SpacePublicShare_shareButton"
+            onClick={async () => {
+                const permalinkCreator = new RoomPermalinkCreator(space);
+                permalinkCreator.load();
+                const success = await copyPlaintext(permalinkCreator.forRoom());
+                const text = success ? _t("Copied!") : _t("Failed to copy");
+                setCopiedText(text);
+                await sleep(10);
+                if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time
+                    setCopiedText(_t("Click to copy"));
+                }
+            }}
+        >
+            { _t("Share invite link") }
+            <span>{ copiedText }</span>
+        </AccessibleButton>
+        <AccessibleButton
+            className="mx_SpacePublicShare_inviteButton"
+            onClick={() => {
+                showSpaceInviteDialog(space.roomId);
+                onFinished();
+            }}
+        >
+            { _t("Invite by email or username") }
+        </AccessibleButton>
+    </div>;
+};
+
+export default SpacePublicShare;
diff --git a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts b/src/dispatcher/payloads/SetRightPanelPhasePayload.ts
index 4126e8a669..430fad6145 100644
--- a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts
+++ b/src/dispatcher/payloads/SetRightPanelPhasePayload.ts
@@ -15,6 +15,7 @@ limitations under the License.
 */
 
 import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import { Room } from "matrix-js-sdk/src/models/room";
 import { RoomMember } from "matrix-js-sdk/src/models/room-member";
 import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
 import { ActionPayload } from "../payloads";
@@ -35,4 +36,5 @@ export interface SetRightPanelPhaseRefireParams {
     // XXX: The type for event should 'view_3pid_invite' action's payload
     event?: any;
     widgetId?: string;
+    space?: Room;
 }
diff --git a/src/hooks/useStateArray.ts b/src/hooks/useStateArray.ts
new file mode 100644
index 0000000000..e8ff6efff0
--- /dev/null
+++ b/src/hooks/useStateArray.ts
@@ -0,0 +1,29 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {useState} from "react";
+
+// Hook to simplify managing state of arrays of a common type
+export const useStateArray = <T>(initialSize: number, initialState: T | T[]): [T[], (i: number, v: T) => void] => {
+    const [data, setData] = useState<T[]>(() => {
+        return Array.isArray(initialState) ? initialState : new Array(initialSize).fill(initialState);
+    });
+    return [data, (index: number, value: T) => setData(data => {
+        const copy = [...data];
+        copy[index] = value;
+        return copy;
+    })]
+};
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 1b29e65b40..ae12b195a0 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -998,6 +998,11 @@
     "Expand space panel": "Expand space panel",
     "Collapse space panel": "Collapse space panel",
     "Home": "Home",
+    "Click to copy": "Click to copy",
+    "Copied!": "Copied!",
+    "Failed to copy": "Failed to copy",
+    "Share invite link": "Share invite link",
+    "Invite by email or username": "Invite by email or username",
     "Remove": "Remove",
     "This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
     "This bridge is managed by <user />.": "This bridge is managed by <user />.",
@@ -1814,8 +1819,6 @@
     "%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
     "Click here to see older messages.": "Click here to see older messages.",
     "This room is a continuation of another conversation.": "This room is a continuation of another conversation.",
-    "Copied!": "Copied!",
-    "Failed to copy": "Failed to copy",
     "Add an Integration": "Add an Integration",
     "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?": "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?",
     "Edited at %(date)s": "Edited at %(date)s",
@@ -2164,8 +2167,11 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
     "Go": "Go",
+    "Invite to this space": "Invite to this space",
     "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
     "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
+    "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
+    "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
     "Transfer": "Transfer",
     "a new master key signature": "a new master key signature",
     "a new cross-signing key signature": "a new cross-signing key signature",
@@ -2550,6 +2556,37 @@
     "Failed to reject invite": "Failed to reject invite",
     "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
     "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
+    "Accept Invite": "Accept Invite",
+    "%(count)s members|other": "%(count)s members",
+    "%(count)s members|one": "%(count)s member",
+    "<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",
+    "You have been invited to <name/>": "You have been invited to <name/>",
+    "Your public space <name/>": "Your public space <name/>",
+    "Your private space <name/>": "Your private space <name/>",
+    "Welcome to <name/>": "Welcome to <name/>",
+    "Random": "Random",
+    "Support": "Support",
+    "Room name": "Room name",
+    "Failed to create initial space rooms": "Failed to create initial space rooms",
+    "Skip for now": "Skip for now",
+    "Creating rooms...": "Creating rooms...",
+    "Share your public space": "Share your public space",
+    "At the moment only you can see it.": "At the moment only you can see it.",
+    "Finish": "Finish",
+    "Who are you working with?": "Who are you working with?",
+    "Ensure the right people have access to the space.": "Ensure the right people have access to the space.",
+    "Just Me": "Just Me",
+    "A private space just for you": "A private space just for you",
+    "Me and my teammates": "Me and my teammates",
+    "A private space for you and your teammates": "A private space for you and your teammates",
+    "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s",
+    "Invite your teammates": "Invite your teammates",
+    "Invite by username": "Invite by username",
+    "Inviting...": "Inviting...",
+    "What discussions do you want to have?": "What discussions do you want to have?",
+    "We'll create rooms for each topic.": "We'll create rooms for each topic.",
+    "What projects are you working on?": "What projects are you working on?",
+    "We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.",
     "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
     "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
     "Failed to load timeline position": "Failed to load timeline position",
diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts
index 11b51dfc2d..aea78c7460 100644
--- a/src/stores/RightPanelStorePhases.ts
+++ b/src/stores/RightPanelStorePhases.ts
@@ -31,6 +31,11 @@ export enum RightPanelPhases {
     GroupRoomList = 'GroupRoomList',
     GroupRoomInfo = 'GroupRoomInfo',
     GroupMemberInfo = 'GroupMemberInfo',
+
+    // Space stuff
+    SpaceMemberList = "SpaceMemberList",
+    SpaceMemberInfo = "SpaceMemberInfo",
+    Space3pidMemberInfo = "Space3pidMemberInfo",
 }
 
 // These are the phases that are safe to persist (the ones that don't require additional
@@ -43,3 +48,10 @@ export const RIGHT_PANEL_PHASES_NO_ARGS = [
     RightPanelPhases.GroupMemberList,
     RightPanelPhases.GroupRoomList,
 ];
+
+// Subset of phases visible in the Space View
+export const RIGHT_PANEL_SPACE_PHASES = [
+    RightPanelPhases.SpaceMemberList,
+    RightPanelPhases.Space3pidMemberInfo,
+    RightPanelPhases.SpaceMemberInfo,
+];
diff --git a/src/utils/space.ts b/src/utils/space.ts
new file mode 100644
index 0000000000..85faedf5d6
--- /dev/null
+++ b/src/utils/space.ts
@@ -0,0 +1,28 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {Room} from "matrix-js-sdk/src/models/room";
+import {MatrixClient} from "matrix-js-sdk/src/client";
+import {EventType} from "matrix-js-sdk/src/@types/event";
+
+export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
+    const userId = cli.getUserId();
+    return space.getMyMembership() === "join"
+        && (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId)
+            || space.currentState.maySendStateEvent(EventType.RoomName, userId)
+            || space.currentState.maySendStateEvent(EventType.RoomTopic, userId)
+            || space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId));
+};

From 1a7a0e619d72eb3c0ec3f3626ebd802195a27a07 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 1 Mar 2021 19:05:50 +0000
Subject: [PATCH 237/389] extend createRoom for creating rooms in a space

---
 src/createRoom.ts                  | 17 +++++++++++++++++
 src/utils/permalinks/Permalinks.js | 16 ++++++++++++++--
 src/utils/space.ts                 | 11 +++++++++++
 3 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/src/createRoom.ts b/src/createRoom.ts
index 00a970eedc..a5343076ac 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -17,6 +17,7 @@ limitations under the License.
 
 import { MatrixClient } from "matrix-js-sdk/src/client";
 import { Room } from "matrix-js-sdk/src/models/room";
+import { EventType } from "matrix-js-sdk/src/@types/event";
 
 import { MatrixClientPeg } from './MatrixClientPeg';
 import Modal from './Modal';
@@ -31,6 +32,8 @@ import GroupStore from "./stores/GroupStore";
 import CountlyAnalytics from "./CountlyAnalytics";
 import { isJoinedOrNearlyJoined } from "./utils/membership";
 import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
+import SpaceStore from "./stores/SpaceStore";
+import { makeSpaceParentEvent } from "./utils/space";
 
 // we define a number of interfaces which take their names from the js-sdk
 /* eslint-disable camelcase */
@@ -84,6 +87,7 @@ export interface IOpts {
     inlineErrors?: boolean;
     andView?: boolean;
     associatedWithCommunity?: string;
+    parentSpace?: Room;
 }
 
 /**
@@ -175,6 +179,16 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
         });
     }
 
+    if (opts.parentSpace) {
+        opts.createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true));
+        opts.createOpts.initial_state.push({
+            type: EventType.RoomHistoryVisibility,
+            content: {
+                "history_visibility": opts.createOpts.preset === Preset.PublicChat ? "world_readable" : "invited",
+            },
+        });
+    }
+
     let modal;
     if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
 
@@ -189,6 +203,9 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
             return Promise.resolve();
         }
     }).then(() => {
+        if (opts.parentSpace) {
+            return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], true);
+        }
         if (opts.associatedWithCommunity) {
             return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false);
         }
diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.js
index 086abc669d..bcf4d87136 100644
--- a/src/utils/permalinks/Permalinks.js
+++ b/src/utils/permalinks/Permalinks.js
@@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import {MatrixClientPeg} from "../../MatrixClientPeg";
 import isIp from "is-ip";
-import * as utils from 'matrix-js-sdk/src/utils';
+import * as utils from "matrix-js-sdk/src/utils";
+import {Room} from "matrix-js-sdk/src/models/room";
+
+import {MatrixClientPeg} from "../../MatrixClientPeg";
 import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor";
 import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor";
 import ElementPermalinkConstructor from "./ElementPermalinkConstructor";
@@ -121,6 +123,10 @@ export class RoomPermalinkCreator {
         this._started = false;
     }
 
+    get serverCandidates() {
+        return this._serverCandidates;
+    }
+
     isStarted() {
         return this._started;
     }
@@ -451,3 +457,9 @@ function isHostnameIpAddress(hostname) {
 
     return isIp(hostname);
 }
+
+export const calculateRoomVia = (room: Room) => {
+    const permalinkCreator = new RoomPermalinkCreator(room);
+    permalinkCreator.load();
+    return permalinkCreator.serverCandidates;
+};
diff --git a/src/utils/space.ts b/src/utils/space.ts
index 85faedf5d6..98801cabd0 100644
--- a/src/utils/space.ts
+++ b/src/utils/space.ts
@@ -18,6 +18,8 @@ import {Room} from "matrix-js-sdk/src/models/room";
 import {MatrixClient} from "matrix-js-sdk/src/client";
 import {EventType} from "matrix-js-sdk/src/@types/event";
 
+import {calculateRoomVia} from "../utils/permalinks/Permalinks";
+
 export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
     const userId = cli.getUserId();
     return space.getMyMembership() === "join"
@@ -26,3 +28,12 @@ export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
             || space.currentState.maySendStateEvent(EventType.RoomTopic, userId)
             || space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId));
 };
+
+export const makeSpaceParentEvent = (room: Room, canonical = false) => ({
+    type: EventType.SpaceParent,
+    content: {
+        "via": calculateRoomVia(room),
+        "canonical": canonical,
+    },
+    state_key: room.roomId,
+});

From 9cec3828650490018e674bb4d79ebe97768ce3d4 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Sat, 27 Feb 2021 22:46:38 -0700
Subject: [PATCH 238/389] Change sending->sent state to match new designs

For https://github.com/vector-im/element-web/issues/16424
---
 res/css/views/rooms/_EventTile.scss           | 39 ++++++++++++-------
 res/img/element-icons/circle-sending.svg      |  3 ++
 res/img/element-icons/circle-sent.svg         |  4 ++
 res/themes/dark/css/_dark.scss                |  3 --
 res/themes/legacy-dark/css/_legacy-dark.scss  |  3 --
 .../legacy-light/css/_legacy-light.scss       |  2 -
 res/themes/light/css/_light.scss              |  2 -
 .../views/messages/EditHistoryMessage.js      |  1 +
 src/components/views/rooms/EventTile.js       | 21 +++++++++-
 9 files changed, 54 insertions(+), 24 deletions(-)
 create mode 100644 res/img/element-icons/circle-sending.svg
 create mode 100644 res/img/element-icons/circle-sent.svg

diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 5841cf2853..028d9a7556 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -213,23 +213,36 @@ $left-gutter: 64px;
     color: $accent-fg-color;
 }
 
-.mx_EventTile_encrypting {
-    color: $event-encrypting-color !important;
-}
-
-.mx_EventTile_sending {
-    color: $event-sending-color;
-}
-
-.mx_EventTile_sending .mx_UserPill,
-.mx_EventTile_sending .mx_RoomPill {
-    opacity: 0.5;
-}
-
 .mx_EventTile_notSent {
     color: $event-notsent-color;
 }
 
+.mx_EventTile_receiptSent,
+.mx_EventTile_receiptSending {
+    // We don't use `position: relative` on the element because then it won't line
+    // up with the other read receipts
+
+    &::before {
+        background-color: $tertiary-fg-color;
+        mask-repeat: no-repeat;
+        mask-position: center;
+        mask-size: 14px;
+        width: 14px;
+        height: 14px;
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+    }
+}
+.mx_EventTile_receiptSent::before {
+    mask-image: url('$(res)/img/element-icons/circle-sent.svg');
+}
+.mx_EventTile_receiptSending::before {
+    mask-image: url('$(res)/img/element-icons/circle-sending.svg');
+}
+
 .mx_EventTile_contextual {
     opacity: 0.4;
 }
diff --git a/res/img/element-icons/circle-sending.svg b/res/img/element-icons/circle-sending.svg
new file mode 100644
index 0000000000..2d15a0f716
--- /dev/null
+++ b/res/img/element-icons/circle-sending.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="8" cy="8" r="7.5" stroke="#8D99A5"/>
+</svg>
diff --git a/res/img/element-icons/circle-sent.svg b/res/img/element-icons/circle-sent.svg
new file mode 100644
index 0000000000..04a00ceff7
--- /dev/null
+++ b/res/img/element-icons/circle-sent.svg
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16Z" fill="#8D99A5"/>
+<path d="M11.8697 4.95309C11.6784 4.7576 11.3597 4.74731 11.1578 4.93251L6.62066 9.04804L4.95244 7.91627C4.7293 7.77223 4.42116 7.77223 4.20865 7.95742C3.95363 8.1632 3.93238 8.5336 4.14489 8.78053L6.06813 10.9206C6.1 10.9515 6.13188 10.9926 6.17438 11.0132C6.53565 11.3013 7.07756 11.2498 7.37508 10.9L7.40695 10.8589L11.891 5.60128C12.0397 5.41608 12.0397 5.13828 11.8697 4.95309Z" fill="#8D99A5"/>
+</svg>
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index a878aa3cdd..344f012d45 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -137,9 +137,6 @@ $panel-divider-color: transparent;
 $widget-menu-bar-bg-color: $header-panel-bg-color;
 $widget-body-bg-color: rgba(141, 151, 165, 0.2);
 
-// event tile lifecycle
-$event-sending-color: $text-secondary-color;
-
 // event redaction
 $event-redacted-fg-color: #606060;
 $event-redacted-border-color: #000000;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 3e3c299af9..ca3ead9ea8 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -132,9 +132,6 @@ $panel-divider-color: $header-panel-border-color;
 $widget-menu-bar-bg-color: $header-panel-bg-color;
 $widget-body-bg-color: #1A1D23;
 
-// event tile lifecycle
-$event-sending-color: $text-secondary-color;
-
 // event redaction
 $event-redacted-fg-color: #606060;
 $event-redacted-border-color: #000000;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index a740ba155c..fa44c128d0 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -222,8 +222,6 @@ $widget-body-bg-color: #fff;
 $yellow-background: #fff8e3;
 
 // event tile lifecycle
-$event-encrypting-color: #abddbc;
-$event-sending-color: #ddd;
 $event-notsent-color: #f44;
 
 $event-highlight-fg-color: $warning-color;
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 1c89d83c01..ca52d0dcfa 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -222,8 +222,6 @@ $widget-body-bg-color: #FFF;
 $yellow-background: #fff8e3;
 
 // event tile lifecycle
-$event-encrypting-color: #abddbc;
-$event-sending-color: #ddd;
 $event-notsent-color: #f44;
 
 $event-highlight-fg-color: $warning-color;
diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
index df27773a40..6c420a16fc 100644
--- a/src/components/views/messages/EditHistoryMessage.js
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -158,6 +158,7 @@ export default class EditHistoryMessage extends React.PureComponent {
         const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.state.sendStatus) !== -1);
         const classes = classNames({
             "mx_EventTile": true,
+            // Note: we keep these sending state classes for tests, not for our styles
             "mx_EventTile_sending": isSending,
             "mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
         });
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 87fb190678..9110316850 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -454,8 +454,26 @@ export default class EventTile extends React.Component {
     };
 
     getReadAvatars() {
-        // return early if there are no read receipts
+        // return early if there are no read receipts, with our message state if applicable
         if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
+            const room = this.context.getRoom(this.props.mxEvent.getRoomId());
+            const myUserId = MatrixClientPeg.get().getUserId();
+            if (this.props.mxEvent.getSender() === myUserId && room) {
+                // We only search for the most recent 50 events because surely someone will have
+                // sent *something* in that time, even if it is a membership event or something.
+                const readUsers = room.getUsersWhoHaveRead(this.props.mxEvent, 50);
+                const hasBeenRead = readUsers.length === 0 || readUsers.some(u => u !== myUserId);
+                console.log(room.getUsersReadUpTo(this.props.mxEvent));
+                let receipt = null;
+                if (!this.props.eventSendStatus || this.props.eventSendStatus === 'sent') {
+                    if (!hasBeenRead) {
+                        receipt = <span className='mx_EventTile_receiptSent' />;
+                    }
+                } else {
+                    receipt = <span className='mx_EventTile_receiptSending' />;
+                }
+                return <span className="mx_EventTile_readAvatars">{receipt}</span>;
+            }
             return (<span className="mx_EventTile_readAvatars" />);
         }
 
@@ -692,6 +710,7 @@ export default class EventTile extends React.Component {
             mx_EventTile_isEditing: isEditing,
             mx_EventTile_info: isInfoMessage,
             mx_EventTile_12hr: this.props.isTwelveHour,
+            // Note: we keep these sending state classes for tests, not for our styles
             mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
             mx_EventTile_sending: !isEditing && isSending,
             mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent',

From db8978580c80f86a91dd2783a2415065dc49e57b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 1 Mar 2021 16:21:04 -0700
Subject: [PATCH 239/389] Improve special read receipt checking

See comments in code
---
 src/components/views/rooms/EventTile.js | 133 ++++++++++++++++++++----
 1 file changed, 114 insertions(+), 19 deletions(-)

diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 9110316850..01e932dd3a 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -264,6 +264,79 @@ export default class EventTile extends React.Component {
 
         this._tile = createRef();
         this._replyThread = createRef();
+
+        // Throughout the component we manage a read receipt listener to see if our tile still
+        // qualifies for a "sent" or "sending" state (based on their relevant conditions). We
+        // don't want to over-subscribe to the read receipt events being fired, so we use a flag
+        // to determine if we've already subscribed and use a combination of other flags to find
+        // out if we should even be subscribed at all.
+        this._isListeningForReceipts = false;
+    }
+
+    /**
+     * When true, the tile qualifies for some sort of special read receipt. This could be a 'sending'
+     * or 'sent' receipt, for example.
+     * @returns {boolean}
+     * @private
+     */
+    get _isEligibleForSpecialReceipt() {
+        // First, if there are other read receipts then just short-circuit this.
+        if (this.props.readReceipts && this.props.readReceipts.length > 0) return false;
+        if (!this.props.mxEvent) return false;
+
+        // Sanity check (should never happen, but we shouldn't explode if it does)
+        const room = this.context.getRoom(this.props.mxEvent.getRoomId());
+        if (!room) return false;
+
+        // Quickly check to see if the event was sent by us. If it wasn't, it won't qualify for
+        // special read receipts.
+        const myUserId = MatrixClientPeg.get().getUserId();
+        if (this.props.mxEvent.getSender() !== myUserId) return false;
+
+        // Finally, determine if the type is relevant to the user. This notably excludes state
+        // events and pretty much anything that can't be sent by the composer as a message. For
+        // those we rely on local echo giving the impression of things changing, and expect them
+        // to be quick.
+        const simpleSendableEvents = [EventType.Sticker, EventType.RoomMessage, EventType.RoomMessageEncrypted];
+        if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false;
+
+        // Default case
+        return true;
+    }
+
+    get _shouldShowSentReceipt() {
+        // If we're not even eligible, don't show the receipt.
+        if (!this._isEligibleForSpecialReceipt) return false;
+
+        // Check to make sure the sending state is appropriate. A null/undefined send status means
+        // that the message is 'sent', so we're just double checking that it's explicitly not sent.
+        if (this.props.eventSendStatus && this.props.eventSendStatus !== 'sent') return false;
+
+        // No point in doing the complex math if we're not going to even show this special receipt.
+        if (this._shouldShowSendingReceipt) return false;
+
+        // Next we check to see if any newer events have read receipts. If they do then we don't
+        // show our special state - the user already has feedback about their message. We only
+        // search for the most recent 50 events because surely someone will have sent *something*
+        // in that time, even if it is a membership event or something.
+        const room = this.context.getRoom(this.props.mxEvent.getRoomId());
+        const myUserId = MatrixClientPeg.get().getUserId();
+        const readUsers = room.getUsersWhoHaveRead(this.props.mxEvent, 50);
+        const hasBeenRead = readUsers.length === 0 || readUsers.some(u => u !== myUserId);
+        return !hasBeenRead;
+    }
+
+    get _shouldShowSendingReceipt() {
+        // If we're not even eligible, don't show the receipt.
+        if (!this._isEligibleForSpecialReceipt) return false;
+
+        // Check the event send status to see if we are pending. Null/undefined status means the
+        // message was sent, so check for that and 'sent' explicitly.
+        if (!this.props.eventSendStatus || this.props.eventSendStatus === 'sent') return false;
+
+        // Default to showing - there's no other event properties/behaviours we care about at
+        // this point.
+        return true;
     }
 
     // TODO: [REACT-WARNING] Move into constructor
@@ -281,6 +354,11 @@ export default class EventTile extends React.Component {
         if (this.props.showReactions) {
             this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
         }
+
+        if (this._shouldShowSentReceipt || this._shouldShowSendingReceipt) {
+            client.on("Room.receipt", this._onRoomReceipt);
+            this._isListeningForReceipts = true;
+        }
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@@ -305,12 +383,40 @@ export default class EventTile extends React.Component {
         const client = this.context;
         client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
         client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
+        client.removeListener("Room.receipt", this._onRoomReceipt);
+        this._isListeningForReceipts = false;
         this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
         if (this.props.showReactions) {
             this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
         }
     }
 
+    componentDidUpdate(prevProps, prevState, snapshot) {
+        // If we're not listening for receipts and expect to be, register a listener.
+        if (!this._isListeningForReceipts && (this._shouldShowSentReceipt || this._shouldShowSendingReceipt)) {
+            this.context.on("Room.receipt", this._onRoomReceipt);
+            this._isListeningForReceipts = true;
+        }
+    }
+
+    _onRoomReceipt = (ev, room) => {
+        // ignore events for other rooms
+        const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
+        if (room !== tileRoom) return;
+
+        if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt && !this._isListeningForReceipts) {
+            return;
+        }
+
+        this.forceUpdate(() => {
+            // Per elsewhere in this file, we can remove the listener once we will have no further purpose for it.
+            if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt) {
+                this.context.removeListener("Room.receipt", this._onRoomReceipt);
+                this._isListeningForReceipts = false;
+            }
+        });
+    };
+
     /** called when the event is decrypted after we show it.
      */
     _onDecrypted = () => {
@@ -454,26 +560,15 @@ export default class EventTile extends React.Component {
     };
 
     getReadAvatars() {
-        // return early if there are no read receipts, with our message state if applicable
+        if (this._shouldShowSentReceipt) {
+            return <span className="mx_EventTile_readAvatars"><span className='mx_EventTile_receiptSent' /></span>;
+        }
+        if (this._shouldShowSendingReceipt) {
+            return <span className="mx_EventTile_readAvatars"><span className='mx_EventTile_receiptSending' /></span>;
+        }
+
+        // return early if there are no read receipts
         if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
-            const room = this.context.getRoom(this.props.mxEvent.getRoomId());
-            const myUserId = MatrixClientPeg.get().getUserId();
-            if (this.props.mxEvent.getSender() === myUserId && room) {
-                // We only search for the most recent 50 events because surely someone will have
-                // sent *something* in that time, even if it is a membership event or something.
-                const readUsers = room.getUsersWhoHaveRead(this.props.mxEvent, 50);
-                const hasBeenRead = readUsers.length === 0 || readUsers.some(u => u !== myUserId);
-                console.log(room.getUsersReadUpTo(this.props.mxEvent));
-                let receipt = null;
-                if (!this.props.eventSendStatus || this.props.eventSendStatus === 'sent') {
-                    if (!hasBeenRead) {
-                        receipt = <span className='mx_EventTile_receiptSent' />;
-                    }
-                } else {
-                    receipt = <span className='mx_EventTile_receiptSending' />;
-                }
-                return <span className="mx_EventTile_readAvatars">{receipt}</span>;
-            }
             return (<span className="mx_EventTile_readAvatars" />);
         }
 

From 6fcb4c7cd2ddf0a5fc64ff1796d079389a3d8482 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 2 Mar 2021 07:37:00 +0100
Subject: [PATCH 240/389] Fix quote

Co-authored-by: Travis Ralston <travpc@gmail.com>
---
 .../views/settings/tabs/user/PreferencesUserSettingsTab.js      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index 5e1c2e7288..ae9cad4cfa 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -48,7 +48,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'showRedactions',
         'enableSyntaxHighlightLanguageDetection',
         'expandCodeByDefault',
-        `scrollToBottomOnMessageSent`,
+        'scrollToBottomOnMessageSent',
         'showCodeLineNumbers',
         'showJoinLeaves',
         'showAvatarChanges',

From ebedd3cbcbadaa8d945f430f5934ab7215e6b0c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 2 Mar 2021 07:41:14 +0100
Subject: [PATCH 241/389] Remove space

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

diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index b3ef8c3cc8..9d19c212c4 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -154,7 +154,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
             fileDropTarget = (
                 <div className="mx_RoomView_fileDropTarget">
                     <img
-                        src={require( "../../../../res/img/upload-big.svg")}
+                        src={require("../../../../res/img/upload-big.svg")}
                         className="mx_RoomView_fileDropTarget_image"
                     />
                     { _t("Drop file here to upload") }

From ff00683f321a0369bc41836fdc55007c38dfd75e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 2 Mar 2021 07:42:07 +0100
Subject: [PATCH 242/389] Use ===

Co-authored-by: Travis Ralston <travpc@gmail.com>
---
 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 5b79f23e0b..4a58e21820 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1159,7 +1159,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             dragCounter: this.state.dragCounter - 1,
         });
 
-        if (this.state.dragCounter == 0) {
+        if (this.state.dragCounter === 0) {
             this.setState({
                 draggingFile: false,
             });

From 8efe7dcaa11b2de16a4f77cb6d0cbaae0fb6d3bc Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 09:51:11 +0000
Subject: [PATCH 243/389] Decorate Right Panel cards with Space header for when
 viewing it in that context

---
 res/css/structures/_RightPanel.scss           | 17 ++++++
 res/css/views/rooms/_MemberInfo.scss          |  1 +
 res/css/views/rooms/_MemberList.scss          |  4 ++
 src/components/views/right_panel/UserInfo.tsx | 57 +++++++++++++------
 src/components/views/rooms/MemberList.js      | 22 ++++++-
 .../views/rooms/ThirdPartyMemberInfo.js       | 19 +++++--
 src/i18n/strings/en_EN.json                   |  3 +-
 7 files changed, 98 insertions(+), 25 deletions(-)

diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss
index 5bf0d953f3..5515fe4060 100644
--- a/res/css/structures/_RightPanel.scss
+++ b/res/css/structures/_RightPanel.scss
@@ -160,3 +160,20 @@ limitations under the License.
         mask-position: center;
     }
 }
+
+.mx_RightPanel_scopeHeader {
+    margin: 24px;
+    text-align: center;
+    font-weight: $font-semi-bold;
+    font-size: $font-18px;
+    line-height: $font-22px;
+
+    .mx_BaseAvatar {
+        margin-right: 8px;
+        vertical-align: middle;
+    }
+
+    .mx_BaseAvatar_image {
+        border-radius: 8px;
+    }
+}
diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss
index 182c280217..3f7f83d334 100644
--- a/res/css/views/rooms/_MemberInfo.scss
+++ b/res/css/views/rooms/_MemberInfo.scss
@@ -19,6 +19,7 @@ limitations under the License.
     flex-direction: column;
     flex: 1;
     overflow-y: auto;
+    margin-top: 8px;
 }
 
 .mx_MemberInfo_name {
diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index 1e3506e371..631ddc484f 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -44,6 +44,10 @@ limitations under the License.
     .mx_AutoHideScrollbar {
         flex: 1 1 0;
     }
+
+    .mx_RightPanel_scopeHeader {
+        margin-top: -8px;
+    }
 }
 
 .mx_GroupMemberList_query,
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index a4b5cd0fbb..eb47a56269 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -60,7 +60,9 @@ import QuestionDialog from "../dialogs/QuestionDialog";
 import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
 import InfoDialog from "../dialogs/InfoDialog";
 import { EventType } from "matrix-js-sdk/src/@types/event";
-import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
+import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
+import RoomAvatar from "../avatars/RoomAvatar";
+import RoomName from "../elements/RoomName";
 
 interface IDevice {
     deviceId: string;
@@ -302,7 +304,8 @@ const UserOptionsSection: React.FC<{
     member: RoomMember;
     isIgnored: boolean;
     canInvite: boolean;
-}> = ({member, isIgnored, canInvite}) => {
+    isSpace?: boolean;
+}> = ({member, isIgnored, canInvite, isSpace}) => {
     const cli = useContext(MatrixClientContext);
 
     let ignoreButton = null;
@@ -342,7 +345,7 @@ const UserOptionsSection: React.FC<{
             </AccessibleButton>
         );
 
-        if (member.roomId) {
+        if (member.roomId && !isSpace) {
             const onReadReceiptButton = function() {
                 const room = cli.getRoom(member.roomId);
                 dis.dispatch({
@@ -434,14 +437,18 @@ const UserOptionsSection: React.FC<{
     );
 };
 
-const warnSelfDemote = async () => {
+const warnSelfDemote = async (isSpace) => {
     const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
         title: _t("Demote yourself?"),
         description:
             <div>
-                { _t("You will not be able to undo this change as you are demoting yourself, " +
-                    "if you are the last privileged user in the room it will be impossible " +
-                    "to regain privileges.") }
+                { isSpace
+                    ? _t("You will not be able to undo this change as you are demoting yourself, " +
+                        "if you are the last privileged user in the space it will be impossible " +
+                        "to regain privileges.")
+                    : _t("You will not be able to undo this change as you are demoting yourself, " +
+                        "if you are the last privileged user in the room it will be impossible " +
+                        "to regain privileges.") }
             </div>,
         button: _t("Demote"),
     });
@@ -717,7 +724,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({member, room, powerLevels,
         // if muting self, warn as it may be irreversible
         if (target === cli.getUserId()) {
             try {
-                if (!(await warnSelfDemote())) return;
+                if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
             } catch (e) {
                 console.error("Failed to warn about self demotion: ", e);
                 return;
@@ -806,7 +813,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
     if (canAffectUser && me.powerLevel >= kickPowerLevel) {
         kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
     }
-    if (me.powerLevel >= redactPowerLevel) {
+    if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
         redactButton = (
             <RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
         );
@@ -1085,7 +1092,7 @@ const PowerLevelEditor: React.FC<{
         } else if (myUserId === target) {
             // If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
             try {
-                if (!(await warnSelfDemote())) return;
+                if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
             } catch (e) {
                 console.error("Failed to warn about self demotion: ", e);
             }
@@ -1315,12 +1322,10 @@ const BasicUserInfo: React.FC<{
     if (!isRoomEncrypted) {
         if (!cryptoEnabled) {
             text = _t("This client does not support end-to-end encryption.");
-        } else if (room) {
+        } else if (room && !room.isSpaceRoom()) {
             text = _t("Messages in this room are not end-to-end encrypted.");
-        } else {
-            // TODO what to render for GroupMember
         }
-    } else {
+    } else if (!room.isSpaceRoom()) {
         text = _t("Messages in this room are end-to-end encrypted.");
     }
 
@@ -1381,7 +1386,9 @@ const BasicUserInfo: React.FC<{
         <UserOptionsSection
             canInvite={roomPermissions.canInvite}
             isIgnored={isIgnored}
-            member={member} />
+            member={member}
+            isSpace={room?.isSpaceRoom()}
+        />
 
         { adminToolsContainer }
 
@@ -1498,7 +1505,7 @@ interface IProps {
     user: Member;
     groupId?: string;
     room?: Room;
-    phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo;
+    phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
     onClose(): void;
 }
 
@@ -1542,7 +1549,9 @@ const UserInfo: React.FC<Props> = ({
         previousPhase = RightPanelPhases.RoomMemberInfo;
         refireParams = {member: member};
     } else if (room) {
-        previousPhase = RightPanelPhases.RoomMemberList;
+        previousPhase = previousPhase = room.isSpaceRoom()
+            ? RightPanelPhases.SpaceMemberList
+            : RightPanelPhases.RoomMemberList;
     }
 
     const onEncryptionPanelClose = () => {
@@ -1557,6 +1566,7 @@ const UserInfo: React.FC<Props> = ({
     switch (phase) {
         case RightPanelPhases.RoomMemberInfo:
         case RightPanelPhases.GroupMemberInfo:
+        case RightPanelPhases.SpaceMemberInfo:
             content = (
                 <BasicUserInfo
                     room={room}
@@ -1587,7 +1597,18 @@ const UserInfo: React.FC<Props> = ({
         }
     }
 
-    const header = <UserInfoHeader member={member} e2eStatus={e2eStatus} />;
+    let scopeHeader;
+    if (room?.isSpaceRoom()) {
+        scopeHeader = <div className="mx_RightPanel_scopeHeader">
+            <RoomAvatar room={room} height={32} width={32} />
+            <RoomName room={room} />
+        </div>;
+    }
+
+    const header = <React.Fragment>
+        { scopeHeader }
+        <UserInfoHeader member={member} e2eStatus={e2eStatus} />
+    </React.Fragment>;
     return <BaseCard
         className={classes.join(" ")}
         header={header}
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index 495a0f0d2c..d4d618c821 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -27,6 +27,8 @@ import * as sdk from "../../../index";
 import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
 import BaseCard from "../right_panel/BaseCard";
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
+import RoomAvatar from "../avatars/RoomAvatar";
+import RoomName from "../elements/RoomName";
 
 const INITIAL_LOAD_NUM_MEMBERS = 30;
 const INITIAL_LOAD_NUM_INVITED = 5;
@@ -456,6 +458,8 @@ export default class MemberList extends React.Component {
             const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
             if (chat && chat.roomId === this.props.roomId) {
                 inviteButtonText = _t("Invite to this community");
+            } else if (room.isSpaceRoom()) {
+                inviteButtonText = _t("Invite to this space");
             }
 
             const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@@ -483,12 +487,26 @@ export default class MemberList extends React.Component {
                 onSearch={ this.onSearchQueryChanged } />
         );
 
+        let previousPhase = RightPanelPhases.RoomSummary;
+        // We have no previousPhase for when viewing a MemberList from a Space
+        let scopeHeader;
+        if (room?.isSpaceRoom()) {
+            previousPhase = undefined;
+            scopeHeader = <div className="mx_RightPanel_scopeHeader">
+                <RoomAvatar room={room} height={32} width={32} />
+                <RoomName room={room} />
+            </div>;
+        }
+
         return <BaseCard
             className="mx_MemberList"
-            header={inviteButton}
+            header={<React.Fragment>
+                { scopeHeader }
+                { inviteButton }
+            </React.Fragment>}
             footer={footer}
             onClose={this.props.onClose}
-            previousPhase={RightPanelPhases.RoomSummary}
+            previousPhase={previousPhase}
         >
             <div className="mx_MemberList_wrapper">
                 <TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.js
index 3a7042ebd2..73510c2b4f 100644
--- a/src/components/views/rooms/ThirdPartyMemberInfo.js
+++ b/src/components/views/rooms/ThirdPartyMemberInfo.js
@@ -23,6 +23,8 @@ import dis from "../../../dispatcher/dispatcher";
 import * as sdk from "../../../index";
 import Modal from "../../../Modal";
 import {isValid3pidInvite} from "../../../RoomInvite";
+import RoomAvatar from "../avatars/RoomAvatar";
+import RoomName from "../elements/RoomName";
 
 export default class ThirdPartyMemberInfo extends React.Component {
     static propTypes = {
@@ -32,14 +34,14 @@ export default class ThirdPartyMemberInfo extends React.Component {
     constructor(props) {
         super(props);
 
-        const room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
-        const me = room.getMember(MatrixClientPeg.get().getUserId());
-        const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
+        this.room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
+        const me = this.room.getMember(MatrixClientPeg.get().getUserId());
+        const powerLevels = this.room.currentState.getStateEvents("m.room.power_levels", "");
 
         let kickLevel = powerLevels ? powerLevels.getContent().kick : 50;
         if (typeof(kickLevel) !== 'number') kickLevel = 50;
 
-        const sender = room.getMember(this.props.event.getSender());
+        const sender = this.room.getMember(this.props.event.getSender());
 
         this.state = {
             stateKey: this.props.event.getStateKey(),
@@ -119,9 +121,18 @@ export default class ThirdPartyMemberInfo extends React.Component {
             );
         }
 
+        let scopeHeader;
+        if (this.room.isSpaceRoom()) {
+            scopeHeader = <div className="mx_RightPanel_scopeHeader">
+                <RoomAvatar room={this.room} height={32} width={32} />
+                <RoomName room={this.room} />
+            </div>;
+        }
+
         // We shamelessly rip off the MemberInfo styles here.
         return (
             <div className="mx_MemberInfo" role="tabpanel">
+                { scopeHeader }
                 <div className="mx_MemberInfo_name">
                     <AccessibleButton className="mx_MemberInfo_cancel"
                         onClick={this.onCancel}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index ae12b195a0..472fd9b1e7 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1436,6 +1436,7 @@
     "and %(count)s others...|one": "and one other...",
     "Invite to this room": "Invite to this room",
     "Invite to this community": "Invite to this community",
+    "Invite to this space": "Invite to this space",
     "Invited": "Invited",
     "Filter room members": "Filter room members",
     "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
@@ -1698,6 +1699,7 @@
     "Share Link to User": "Share Link to User",
     "Direct message": "Direct message",
     "Demote yourself?": "Demote yourself?",
+    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.",
     "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
     "Demote": "Demote",
     "Disinvite": "Disinvite",
@@ -2167,7 +2169,6 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
     "Go": "Go",
-    "Invite to this space": "Invite to this space",
     "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
     "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
     "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",

From dfd0aaffe348e85c32b13b702e6a7cf0feb048c5 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 09:55:55 +0000
Subject: [PATCH 244/389] Iterate copy for some global warning prompts for
 spaces

---
 src/components/structures/MatrixChat.tsx | 14 ++++++++++----
 src/i18n/strings/en_EN.json              |  3 +++
 2 files changed, 13 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 8e3d3e6b5f..d9d8b659c9 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -1072,6 +1072,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
 
     private leaveRoomWarnings(roomId: string) {
         const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
+        const isSpace = roomToLeave?.isSpaceRoom();
         // Show a warning if there are additional complications.
         const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
         const warnings = [];
@@ -1081,7 +1082,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                 warnings.push((
                     <span className="warning" key="non_public_warning">
                         {' '/* Whitespace, otherwise the sentences get smashed together */ }
-                        { _t("This room is not public. You will not be able to rejoin without an invite.") }
+                        { isSpace
+                            ? _t("This space is not public. You will not be able to rejoin without an invite.")
+                            : _t("This room is not public. You will not be able to rejoin without an invite.") }
                     </span>
                 ));
             }
@@ -1094,11 +1097,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
         const warnings = this.leaveRoomWarnings(roomId);
 
-        Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
-            title: _t("Leave room"),
+        const isSpace = roomToLeave?.isSpaceRoom();
+        Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
+            title: isSpace ? _t("Leave space") : _t("Leave room"),
             description: (
                 <span>
-                    { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
+                    { isSpace
+                        ? _t("Are you sure you want to leave the space '%(spaceName)s'?", {spaceName: roomToLeave.name})
+                        : _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
                     { warnings }
                 </span>
             ),
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 472fd9b1e7..6603a83496 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2493,7 +2493,10 @@
     "Failed to reject invitation": "Failed to reject invitation",
     "Cannot create rooms in this community": "Cannot create rooms in this community",
     "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",
+    "This space is not public. You will not be able to rejoin without an invite.": "This space is not public. You will not be able to rejoin without an invite.",
     "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
+    "Leave space": "Leave space",
+    "Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?",
     "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
     "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
     "Signed Out": "Signed Out",

From 926e226a784d5bd66dee9f788618c060cb693e46 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 10:07:43 +0000
Subject: [PATCH 245/389] Add Invite CTA to Space View

---
 res/css/structures/_SpaceRoomView.scss        | 54 +++++++++++++++++++
 src/RoomInvite.js                             | 14 ++---
 src/components/structures/SpaceRoomView.tsx   | 18 ++++++-
 .../views/spaces/SpacePublicShare.tsx         |  4 +-
 src/i18n/strings/en_EN.json                   |  1 +
 5 files changed, 78 insertions(+), 13 deletions(-)

diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 559f405e59..946856eed3 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -135,6 +135,60 @@ $SpaceRoomViewInnerWidth: 428px;
                 padding: 8px 22px;
             }
         }
+
+        .mx_SpaceRoomView_landing_adminButtons {
+            margin-top: 32px;
+
+            .mx_AccessibleButton {
+                position: relative;
+                width: 160px;
+                height: 124px;
+                box-sizing: border-box;
+                padding: 72px 16px 0;
+                border-radius: 12px;
+                border: 1px solid $space-button-outline-color;
+                margin-right: 28px;
+                margin-bottom: 28px;
+                font-size: $font-14px;
+                display: inline-block;
+                vertical-align: bottom;
+
+                &:last-child {
+                    margin-right: 0;
+                }
+
+                &:hover {
+                    background-color: rgba(141, 151, 165, 0.1);
+                }
+
+                &::before, &::after {
+                    position: absolute;
+                    content: "";
+                    left: 16px;
+                    top: 16px;
+                    height: 40px;
+                    width: 40px;
+                    border-radius: 20px;
+                }
+
+                &::after {
+                    mask-position: center;
+                    mask-size: 30px;
+                    mask-repeat: no-repeat;
+                    background: #ffffff; // white icon fill
+                }
+
+                &.mx_SpaceRoomView_landing_inviteButton {
+                    &::before {
+                        background-color: $accent-color;
+                    }
+
+                    &::after {
+                        mask-image: url('$(res)/img/element-icons/room/invite.svg');
+                    }
+                }
+            }
+        }
     }
 
     .mx_SpaceRoomView_privateScope {
diff --git a/src/RoomInvite.js b/src/RoomInvite.js
index 728ae11e79..503411d2b3 100644
--- a/src/RoomInvite.js
+++ b/src/RoomInvite.js
@@ -50,10 +50,13 @@ export function showStartChatInviteDialog(initialText) {
 }
 
 export function showRoomInviteDialog(roomId) {
+    const isSpace = MatrixClientPeg.get()?.getRoom(roomId)?.isSpaceRoom();
     // This dialog handles the room creation internally - we don't need to worry about it.
-    const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
     Modal.createTrackedDialog(
-        'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
+        "Invite Users", isSpace ? "Space" : "Room", InviteDialog, {
+            kind: isSpace ? KIND_SPACE_INVITE : KIND_INVITE,
+            roomId,
+        },
         /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
     );
 }
@@ -75,13 +78,6 @@ export function showCommunityInviteDialog(communityId) {
     }
 }
 
-export const showSpaceInviteDialog = (roomId) => {
-    Modal.createTrackedDialog("Invite Users", "Space", InviteDialog, {
-        kind: KIND_SPACE_INVITE,
-        roomId,
-    }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
-};
-
 /**
  * Checks if the given MatrixEvent is a valid 3rd party user invite.
  * @param {MatrixEvent} event The event to check
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 6c64df31eb..9e73b97d5a 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -25,7 +25,7 @@ import AccessibleButton from "../views/elements/AccessibleButton";
 import RoomName from "../views/elements/RoomName";
 import RoomTopic from "../views/elements/RoomTopic";
 import FormButton from "../views/elements/FormButton";
-import {inviteMultipleToRoom, showSpaceInviteDialog} from "../../RoomInvite";
+import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
 import {useRoomMembers} from "../../hooks/useRoomMembers";
 import createRoom, {IOpts, Preset} from "../../createRoom";
 import Field from "../views/elements/Field";
@@ -108,6 +108,17 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
         </div>;
     }
 
+    let inviteButton;
+    if (myMembership === "join" && space.canInvite(userId)) {
+        inviteButton = (
+            <AccessibleButton className="mx_SpaceRoomView_landing_inviteButton" onClick={() => {
+                showRoomInviteDialog(space.roomId);
+            }}>
+                { _t("Invite people") }
+            </AccessibleButton>
+        );
+    }
+
     return <div className="mx_SpaceRoomView_landing">
         <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
         <div className="mx_SpaceRoomView_landing_name">
@@ -167,6 +178,9 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
             <RoomTopic room={space} />
         </div>
         { joinButtons }
+        <div className="mx_SpaceRoomView_landing_adminButtons">
+            { inviteButton }
+        </div>
     </div>;
 };
 
@@ -361,7 +375,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
         <div className="mx_SpaceRoomView_inviteTeammates_buttons">
             <AccessibleButton
                 className="mx_SpaceRoomView_inviteTeammates_inviteDialogButton"
-                onClick={() => showSpaceInviteDialog(space.roomId)}
+                onClick={() => showRoomInviteDialog(space.roomId)}
             >
                 { _t("Invite by username") }
             </AccessibleButton>
diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx
index 064d1640a2..3930c1db16 100644
--- a/src/components/views/spaces/SpacePublicShare.tsx
+++ b/src/components/views/spaces/SpacePublicShare.tsx
@@ -22,7 +22,7 @@ import AccessibleButton from "../elements/AccessibleButton";
 import {copyPlaintext} from "../../../utils/strings";
 import {sleep} from "../../../utils/promise";
 import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
-import {showSpaceInviteDialog} from "../../../RoomInvite";
+import {showRoomInviteDialog} from "../../../RoomInvite";
 
 interface IProps {
     space: Room;
@@ -53,7 +53,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
         <AccessibleButton
             className="mx_SpacePublicShare_inviteButton"
             onClick={() => {
-                showSpaceInviteDialog(space.roomId);
+                showRoomInviteDialog(space.roomId);
                 onFinished();
             }}
         >
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 6603a83496..5f3d293571 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2561,6 +2561,7 @@
     "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
     "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
     "Accept Invite": "Accept Invite",
+    "Invite people": "Invite people",
     "%(count)s members|other": "%(count)s members",
     "%(count)s members|one": "%(count)s member",
     "<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",

From 0a4c0b69b0fa250fa01c3ff1a88ba272dc5ef64e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 2 Mar 2021 12:07:33 +0100
Subject: [PATCH 246/389] Move fileDropTarget to RoomView
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/structures/RoomView.tsx  | 17 +++++++++++++++--
 src/components/views/rooms/AuxPanel.tsx | 17 -----------------
 2 files changed, 15 insertions(+), 19 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 4a58e21820..af7b8ee704 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1782,6 +1782,19 @@ export default class RoomView extends React.Component<IProps, IState> {
             }
         }
 
+        let fileDropTarget = null;
+        if (this.state.draggingFile) {
+            fileDropTarget = (
+                <div className="mx_RoomView_fileDropTarget">
+                    <img
+                        src={require("../../../res/img/upload-big.svg")}
+                        className="mx_RoomView_fileDropTarget_image"
+                    />
+                    { _t("Drop file here to upload") }
+                </div>
+            );
+        }
+
         // We have successfully loaded this room, and are not previewing.
         // Display the "normal" room view.
 
@@ -1893,7 +1906,6 @@ export default class RoomView extends React.Component<IProps, IState> {
                 room={this.state.room}
                 fullHeight={false}
                 userId={this.context.credentials.userId}
-                draggingFile={this.state.draggingFile}
                 maxHeight={this.state.auxPanelMaxHeight}
                 showApps={this.state.showApps}
                 onResize={this.onResize}
@@ -2059,8 +2071,9 @@ export default class RoomView extends React.Component<IProps, IState> {
                         />
                         <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
                             <div className="mx_RoomView_body">
+                                {auxPanel}
                                 <div className={timelineClasses}>
-                                    {auxPanel}
+                                    {fileDropTarget}
                                     {topUnreadMessagesBar}
                                     {jumpToBottom}
                                     {messagePanel}
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 9d19c212c4..7aa7be42b6 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -35,9 +35,6 @@ interface IProps {
     userId: string,
     showApps: boolean, // Render apps
 
-    // set to true to show the file drop target
-    draggingFile: boolean,
-
     // maxHeight attribute for the aux panel and the video
     // therein
     maxHeight: number,
@@ -149,19 +146,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
     }
 
     render() {
-        let fileDropTarget = null;
-        if (this.props.draggingFile) {
-            fileDropTarget = (
-                <div className="mx_RoomView_fileDropTarget">
-                    <img
-                        src={require("../../../../res/img/upload-big.svg")}
-                        className="mx_RoomView_fileDropTarget_image"
-                    />
-                    { _t("Drop file here to upload") }
-                </div>
-            );
-        }
-
         const callView = (
             <CallViewForRoom
                 roomId={this.props.room.roomId}
@@ -244,7 +228,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
             <AutoHideScrollbar className={classes} style={style} >
                 { stateViews }
                 { appsDrawer }
-                { fileDropTarget }
                 { callView }
                 { this.props.children }
             </AutoHideScrollbar>

From 4476843264eeb61380662eb0040a30edfec42c99 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 2 Mar 2021 12:12:10 +0100
Subject: [PATCH 247/389] Remove unused _t
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/components/views/rooms/AuxPanel.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 7aa7be42b6..c9821d51e3 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -20,7 +20,6 @@ import { Room } from 'matrix-js-sdk/src/models/room'
 import dis from "../../../dispatcher/dispatcher";
 import * as ObjectUtils from '../../../ObjectUtils';
 import AppsDrawer from './AppsDrawer';
-import { _t } from '../../../languageHandler';
 import classNames from 'classnames';
 import RateLimitedFunc from '../../../ratelimitedfunc';
 import SettingsStore from "../../../settings/SettingsStore";

From 831cc7eaa0ecd57a25cff40daf582b02e3fd4e21 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 2 Mar 2021 12:14:36 +0100
Subject: [PATCH 248/389] i18n
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 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 9af8ccc172..5d2c70be03 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1381,7 +1381,6 @@
     "Remove %(phone)s?": "Remove %(phone)s?",
     "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
     "Phone Number": "Phone Number",
-    "Drop file here to upload": "Drop file here to upload",
     "This user has not verified all of their sessions.": "This user has not verified all of their sessions.",
     "You have not verified this user.": "You have not verified this user.",
     "You have verified this user. This user has verified all of their sessions.": "You have verified this user. This user has verified all of their sessions.",
@@ -2513,6 +2512,7 @@
     "No more results": "No more results",
     "Room": "Room",
     "Failed to reject invite": "Failed to reject invite",
+    "Drop file here to upload": "Drop file here to upload",
     "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
     "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
     "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",

From ab4b7b73ea7f8eeb18be6ab7491e2e1649b35969 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 10:34:28 +0000
Subject: [PATCH 249/389] Add a basic Space Settings view

---
 res/css/_components.scss                      |   1 +
 res/css/structures/_SpaceRoomView.scss        |  10 ++
 .../views/dialogs/_SpaceSettingsDialog.scss   |  55 ++++++
 src/components/structures/MatrixChat.tsx      |   4 +
 src/components/structures/SpaceRoomView.tsx   |  12 +-
 .../views/dialogs/SpaceSettingsDialog.tsx     | 162 ++++++++++++++++++
 src/i18n/strings/en_EN.json                   |   8 +
 src/utils/space.ts                            |   9 +
 8 files changed, 260 insertions(+), 1 deletion(-)
 create mode 100644 res/css/views/dialogs/_SpaceSettingsDialog.scss
 create mode 100644 src/components/views/dialogs/SpaceSettingsDialog.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index ca66aa60ec..db73eed3f2 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -91,6 +91,7 @@
 @import "./views/dialogs/_SettingsDialog.scss";
 @import "./views/dialogs/_ShareDialog.scss";
 @import "./views/dialogs/_SlashCommandHelpDialog.scss";
+@import "./views/dialogs/_SpaceSettingsDialog.scss";
 @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
 @import "./views/dialogs/_TermsDialog.scss";
 @import "./views/dialogs/_UploadConfirmDialog.scss";
diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 946856eed3..0a42db130a 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -187,6 +187,16 @@ $SpaceRoomViewInnerWidth: 428px;
                         mask-image: url('$(res)/img/element-icons/room/invite.svg');
                     }
                 }
+
+                &.mx_SpaceRoomView_landing_settingsButton {
+                    &::before {
+                        background-color: #5c56f5;
+                    }
+
+                    &::after {
+                        mask-image: url('$(res)/img/element-icons/settings.svg');
+                    }
+                }
             }
         }
     }
diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss
new file mode 100644
index 0000000000..c1fa539e9b
--- /dev/null
+++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss
@@ -0,0 +1,55 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_SpaceSettingsDialog {
+    width: 480px;
+    color: $primary-fg-color;
+
+    .mx_SpaceSettings_errorText {
+        font-weight: $font-semi-bold;
+        font-size: $font-12px;
+        line-height: $font-15px;
+        color: $notice-primary-color;
+        margin-bottom: 28px;
+    }
+
+    .mx_ToggleSwitch {
+        display: inline-block;
+        vertical-align: middle;
+        margin-left: 16px;
+    }
+
+    .mx_AccessibleButton_kind_danger {
+        margin-top: 28px;
+    }
+
+    .mx_SpaceSettingsDialog_buttons {
+        display: flex;
+        margin-top: 64px;
+
+        .mx_AccessibleButton {
+            display: inline-block;
+        }
+
+        .mx_AccessibleButton_kind_link {
+            margin-left: auto;
+        }
+    }
+
+    .mx_FormButton {
+        padding: 8px 22px;
+    }
+}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index d9d8b659c9..83b3565738 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -1118,6 +1118,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                     const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
 
                     d.finally(() => modal.close());
+                    dis.dispatch({
+                        action: "after_leave_room",
+                        room_id: roomId,
+                    });
                 }
             },
         });
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 9e73b97d5a..49af14017e 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -46,7 +46,7 @@ import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
 import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
 import {useStateArray} from "../../hooks/useStateArray";
 import SpacePublicShare from "../views/spaces/SpacePublicShare";
-import {shouldShowSpaceSettings} from "../../utils/space";
+import {shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
 import MemberAvatar from "../views/avatars/MemberAvatar";
 
 interface IProps {
@@ -119,6 +119,15 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
         );
     }
 
+    let settingsButton;
+    if (shouldShowSpaceSettings(cli, space)) {
+        settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
+            showSpaceSettings(cli, space);
+        }}>
+            { _t("Settings") }
+        </AccessibleButton>;
+    }
+
     return <div className="mx_SpaceRoomView_landing">
         <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
         <div className="mx_SpaceRoomView_landing_name">
@@ -180,6 +189,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
         { joinButtons }
         <div className="mx_SpaceRoomView_landing_adminButtons">
             { inviteButton }
+            { settingsButton }
         </div>
     </div>;
 };
diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx
new file mode 100644
index 0000000000..f6bf5b87e6
--- /dev/null
+++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx
@@ -0,0 +1,162 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useState} from 'react';
+import {Room} from "matrix-js-sdk/src/models/room";
+import {MatrixClient} from "matrix-js-sdk/src/client";
+import {EventType} from "matrix-js-sdk/src/@types/event";
+
+import {_t} from '../../../languageHandler';
+import {IDialogProps} from "./IDialogProps";
+import BaseDialog from "./BaseDialog";
+import DevtoolsDialog from "./DevtoolsDialog";
+import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
+import {getTopic} from "../elements/RoomTopic";
+import {avatarUrlForRoom} from "../../../Avatar";
+import ToggleSwitch from "../elements/ToggleSwitch";
+import AccessibleButton from "../elements/AccessibleButton";
+import FormButton from "../elements/FormButton";
+import Modal from "../../../Modal";
+import defaultDispatcher from "../../../dispatcher/dispatcher";
+import {allSettled} from "../../../utils/promise";
+import {useDispatcher} from "../../../hooks/useDispatcher";
+
+interface IProps extends IDialogProps {
+    matrixClient: MatrixClient;
+    space: Room;
+}
+
+const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFinished }) => {
+    useDispatcher(defaultDispatcher, ({action, ...params}) => {
+        if (action === "after_leave_room" && params.room_id === space.roomId) {
+            onFinished(false);
+        }
+    });
+
+    const [busy, setBusy] = useState(false);
+    const [error, setError] = useState("");
+
+    const userId = cli.getUserId();
+
+    const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
+    const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
+    const avatarChanged = newAvatar !== null;
+
+    const [name, setName] = useState<string>(space.name);
+    const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
+    const nameChanged = name !== space.name;
+
+    const currentTopic = getTopic(space);
+    const [topic, setTopic] = useState<string>(currentTopic);
+    const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
+    const topicChanged = topic !== currentTopic;
+
+    const currentJoinRule = space.getJoinRule();
+    const [joinRule, setJoinRule] = useState(currentJoinRule);
+    const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
+    const joinRuleChanged = joinRule !== currentJoinRule;
+
+    const onSave = async () => {
+        setBusy(true);
+        const promises = [];
+
+        if (avatarChanged) {
+            promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
+                url: await cli.uploadContent(newAvatar),
+            }, ""));
+        }
+
+        if (nameChanged) {
+            promises.push(cli.setRoomName(space.roomId, name));
+        }
+
+        if (topicChanged) {
+            promises.push(cli.setRoomTopic(space.roomId, topic));
+        }
+
+        if (joinRuleChanged) {
+            promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
+        }
+
+        const results = await allSettled(promises);
+        setBusy(false);
+        const failures = results.filter(r => r.status === "rejected");
+        if (failures.length > 0) {
+            console.error("Failed to save space settings: ", failures);
+            setError(_t("Failed to save space settings."));
+        }
+    };
+
+    return <BaseDialog
+        title={_t("Space settings")}
+        className="mx_SpaceSettingsDialog"
+        contentId="mx_SpaceSettingsDialog"
+        onFinished={onFinished}
+        fixedWidth={false}
+    >
+        <div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog">
+            <div>{ _t("Edit settings relating to your space.") }</div>
+
+            { error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
+
+            <SpaceBasicSettings
+                avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
+                avatarDisabled={!canSetAvatar}
+                setAvatar={setNewAvatar}
+                name={name}
+                nameDisabled={!canSetName}
+                setName={setName}
+                topic={topic}
+                topicDisabled={!canSetTopic}
+                setTopic={setTopic}
+            />
+
+            <div>
+                { _t("Make this space private") }
+                <ToggleSwitch
+                    checked={joinRule === "private"}
+                    onChange={checked => setJoinRule(checked ? "private" : "invite")}
+                    disabled={!canSetJoinRule}
+                    aria-label={_t("Make this space private")}
+                />
+            </div>
+
+            <FormButton
+                kind="danger"
+                label={_t("Leave Space")}
+                onClick={() => {
+                    defaultDispatcher.dispatch({
+                        action: "leave_room",
+                        room_id: space.roomId,
+                    });
+                }}
+            />
+
+            <div className="mx_SpaceSettingsDialog_buttons">
+                <AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
+                    { _t("View dev tools") }
+                </AccessibleButton>
+                <AccessibleButton onClick={onFinished} disabled={busy} kind="link">
+                    { _t("Cancel") }
+                </AccessibleButton>
+                <FormButton onClick={onSave} disabled={busy} label={busy ? _t("Saving...") : _t("Save Changes")} />
+            </div>
+        </div>
+    </BaseDialog>;
+};
+
+export default SpaceSettingsDialog;
+
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5f3d293571..cd2fcf1117 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2282,6 +2282,14 @@
     "Link to selected message": "Link to selected message",
     "Copy": "Copy",
     "Command Help": "Command Help",
+    "Failed to save space settings.": "Failed to save space settings.",
+    "Space settings": "Space settings",
+    "Edit settings relating to your space.": "Edit settings relating to your space.",
+    "Make this space private": "Make this space private",
+    "Leave Space": "Leave Space",
+    "View dev tools": "View dev tools",
+    "Saving...": "Saving...",
+    "Save Changes": "Save Changes",
     "To help us prevent this in future, please <a>send us logs</a>.": "To help us prevent this in future, please <a>send us logs</a>.",
     "Missing session data": "Missing session data",
     "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
diff --git a/src/utils/space.ts b/src/utils/space.ts
index 98801cabd0..2ee4d0071e 100644
--- a/src/utils/space.ts
+++ b/src/utils/space.ts
@@ -19,6 +19,8 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
 import {EventType} from "matrix-js-sdk/src/@types/event";
 
 import {calculateRoomVia} from "../utils/permalinks/Permalinks";
+import Modal from "../Modal";
+import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog";
 
 export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
     const userId = cli.getUserId();
@@ -37,3 +39,10 @@ export const makeSpaceParentEvent = (room: Room, canonical = false) => ({
     },
     state_key: room.roomId,
 });
+
+export const showSpaceSettings = (cli: MatrixClient, space: Room) => {
+    Modal.createTrackedDialog("Space Settings", "", SpaceSettingsDialog, {
+        matrixClient: cli,
+        space,
+    }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
+};

From a687b9883ca42c927ec89d20ff066620a10b17ed Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 13:28:05 +0000
Subject: [PATCH 250/389] Add a create room in space CTA to Space View

---
 res/css/structures/_SpaceRoomView.scss         | 10 ++++++++++
 src/components/structures/SpaceRoomView.tsx    | 16 +++++++++++++++-
 .../views/dialogs/CreateRoomDialog.js          |  7 +++++++
 src/i18n/strings/en_EN.json                    |  1 +
 src/utils/space.ts                             | 18 ++++++++++++++++++
 5 files changed, 51 insertions(+), 1 deletion(-)

diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 0a42db130a..eaaaa2f797 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -188,6 +188,16 @@ $SpaceRoomViewInnerWidth: 428px;
                     }
                 }
 
+                &.mx_SpaceRoomView_landing_createButton {
+                    &::before {
+                        background-color: #368bd6;
+                    }
+
+                    &::after {
+                        mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
+                    }
+                }
+
                 &.mx_SpaceRoomView_landing_settingsButton {
                     &::before {
                         background-color: #5c56f5;
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 49af14017e..4159a38cfe 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -46,7 +46,7 @@ import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
 import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
 import {useStateArray} from "../../hooks/useStateArray";
 import SpacePublicShare from "../views/spaces/SpacePublicShare";
-import {shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
+import {showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
 import MemberAvatar from "../views/avatars/MemberAvatar";
 
 interface IProps {
@@ -119,6 +119,19 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
         );
     }
 
+    const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
+
+    let addRoomButtons;
+    if (canAddRooms) {
+        addRoomButtons = <React.Fragment>
+            <AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
+                showCreateNewRoom(cli, space);
+            }}>
+                { _t("Create a new room") }
+            </AccessibleButton>
+        </React.Fragment>;
+    }
+
     let settingsButton;
     if (shouldShowSpaceSettings(cli, space)) {
         settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
@@ -189,6 +202,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
         { joinButtons }
         <div className="mx_SpaceRoomView_landing_adminButtons">
             { inviteButton }
+            { addRoomButtons }
             { settingsButton }
         </div>
     </div>;
diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js
index 2b6bb5e187..0771b0ec45 100644
--- a/src/components/views/dialogs/CreateRoomDialog.js
+++ b/src/components/views/dialogs/CreateRoomDialog.js
@@ -17,6 +17,8 @@ limitations under the License.
 
 import React from 'react';
 import PropTypes from 'prop-types';
+import {Room} from "matrix-js-sdk/src/models/room";
+
 import * as sdk from '../../../index';
 import SdkConfig from '../../../SdkConfig';
 import withValidation from '../elements/Validation';
@@ -30,6 +32,7 @@ export default class CreateRoomDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
         defaultPublic: PropTypes.bool,
+        parentSpace: PropTypes.instanceOf(Room),
     };
 
     constructor(props) {
@@ -85,6 +88,10 @@ export default class CreateRoomDialog extends React.Component {
             opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
         }
 
+        if (this.props.parentSpace) {
+            opts.parentSpace = this.props.parentSpace;
+        }
+
         return opts;
     }
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index cd2fcf1117..aeef76bf22 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2570,6 +2570,7 @@
     "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
     "Accept Invite": "Accept Invite",
     "Invite people": "Invite people",
+    "Create a new room": "Create a new room",
     "%(count)s members|other": "%(count)s members",
     "%(count)s members|one": "%(count)s member",
     "<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",
diff --git a/src/utils/space.ts b/src/utils/space.ts
index 2ee4d0071e..c995b860ee 100644
--- a/src/utils/space.ts
+++ b/src/utils/space.ts
@@ -21,6 +21,8 @@ import {EventType} from "matrix-js-sdk/src/@types/event";
 import {calculateRoomVia} from "../utils/permalinks/Permalinks";
 import Modal from "../Modal";
 import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog";
+import CreateRoomDialog from "../components/views/dialogs/CreateRoomDialog";
+import createRoom, {IOpts} from "../createRoom";
 
 export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
     const userId = cli.getUserId();
@@ -46,3 +48,19 @@ export const showSpaceSettings = (cli: MatrixClient, space: Room) => {
         space,
     }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
 };
+
+export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
+    const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
+        "Space Landing",
+        "Create Room",
+        CreateRoomDialog,
+        {
+            defaultPublic: space.getJoinRule() === "public",
+            parentSpace: space,
+        },
+    );
+    const [shouldCreate, opts] = await modal.finished;
+    if (shouldCreate) {
+        await createRoom(opts);
+    }
+};

From e479edd47a9849a5cf12523b82eba92f1c9fe59b Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 13:32:24 +0000
Subject: [PATCH 251/389] Add an add existing room to space CTA to Space View

---
 res/css/_components.scss                      |   1 +
 res/css/structures/_SpaceRoomView.scss        |  10 +
 .../dialogs/_AddExistingToSpaceDialog.scss    | 185 ++++++++++++++++
 src/components/structures/SpaceRoomView.tsx   |  10 +-
 .../dialogs/AddExistingToSpaceDialog.tsx      | 208 ++++++++++++++++++
 src/i18n/strings/en_EN.json                   |  11 +-
 src/utils/space.ts                            |  15 ++
 7 files changed, 438 insertions(+), 2 deletions(-)
 create mode 100644 res/css/views/dialogs/_AddExistingToSpaceDialog.scss
 create mode 100644 src/components/views/dialogs/AddExistingToSpaceDialog.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index db73eed3f2..8569f62de9 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -58,6 +58,7 @@
 @import "./views/context_menus/_MessageContextMenu.scss";
 @import "./views/context_menus/_StatusMessageContextMenu.scss";
 @import "./views/context_menus/_TagTileContextMenu.scss";
+@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
 @import "./views/dialogs/_AddressPickerDialog.scss";
 @import "./views/dialogs/_Analytics.scss";
 @import "./views/dialogs/_BugReportDialog.scss";
diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index eaaaa2f797..ee60389c59 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -188,6 +188,16 @@ $SpaceRoomViewInnerWidth: 428px;
                     }
                 }
 
+                &.mx_SpaceRoomView_landing_addButton {
+                    &::before {
+                        background-color: #ac3ba8;
+                    }
+
+                    &::after {
+                        mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
+                    }
+                }
+
                 &.mx_SpaceRoomView_landing_createButton {
                     &::before {
                         background-color: #368bd6;
diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
new file mode 100644
index 0000000000..0c9d8e3840
--- /dev/null
+++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
@@ -0,0 +1,185 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_AddExistingToSpaceDialog_wrapper {
+    .mx_Dialog {
+        display: flex;
+        flex-direction: column;
+    }
+}
+
+.mx_AddExistingToSpaceDialog {
+    width: 480px;
+    color: $primary-fg-color;
+    display: flex;
+    flex-direction: column;
+    flex-wrap: nowrap;
+    min-height: 0;
+
+    .mx_Dialog_title {
+        display: flex;
+
+        .mx_BaseAvatar {
+            display: inline-flex;
+            margin: 5px 16px 5px 5px;
+            vertical-align: middle;
+        }
+
+        .mx_BaseAvatar_image {
+            border-radius: 8px;
+            margin: 0;
+            vertical-align: unset;
+        }
+
+        > div {
+            > h1 {
+                font-weight: $font-semi-bold;
+                font-size: $font-18px;
+                line-height: $font-22px;
+                margin: 0;
+            }
+
+            .mx_AddExistingToSpaceDialog_onlySpace {
+                color: $secondary-fg-color;
+                font-size: $font-15px;
+                line-height: $font-24px;
+            }
+        }
+
+        .mx_Dropdown_input {
+            border: none;
+
+            > .mx_Dropdown_option {
+                padding-left: 0;
+                flex: unset;
+                height: unset;
+                color: $secondary-fg-color;
+                font-size: $font-15px;
+                line-height: $font-24px;
+
+                .mx_BaseAvatar {
+                    display: none;
+                }
+            }
+
+            .mx_Dropdown_menu {
+                .mx_AddExistingToSpaceDialog_dropdownOptionActive {
+                    color: $accent-color;
+                    padding-right: 32px;
+                    position: relative;
+
+                    &::before {
+                        content: '';
+                        width: 20px;
+                        height: 20px;
+                        top: 8px;
+                        right: 0;
+                        position: absolute;
+                        mask-position: center;
+                        mask-size: contain;
+                        mask-repeat: no-repeat;
+                        background-color: $accent-color;
+                        mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
+                    }
+                }
+            }
+        }
+    }
+
+    .mx_SearchBox {
+        margin: 0;
+    }
+
+    .mx_AddExistingToSpaceDialog_errorText {
+        font-weight: $font-semi-bold;
+        font-size: $font-12px;
+        line-height: $font-15px;
+        color: $notice-primary-color;
+        margin-bottom: 28px;
+    }
+
+    .mx_AddExistingToSpaceDialog_content {
+        .mx_AddExistingToSpaceDialog_noResults {
+            margin-top: 24px;
+        }
+    }
+
+    .mx_AddExistingToSpaceDialog_section {
+        margin-top: 24px;
+
+        > h3 {
+            margin: 0;
+            color: $secondary-fg-color;
+            font-size: $font-12px;
+            font-weight: $font-semi-bold;
+            line-height: $font-15px;
+        }
+
+        .mx_AddExistingToSpaceDialog_entry {
+            display: flex;
+            margin-top: 12px;
+
+            .mx_BaseAvatar {
+                margin-right: 12px;
+            }
+
+            .mx_AddExistingToSpaceDialog_entry_name {
+                font-size: $font-15px;
+                line-height: 30px;
+                flex-grow: 1;
+            }
+
+            .mx_FormButton {
+                min-width: 92px;
+                font-weight: normal;
+                box-sizing: border-box;
+            }
+        }
+    }
+
+    .mx_AddExistingToSpaceDialog_section_spaces {
+        .mx_BaseAvatar_image {
+            border-radius: 8px;
+        }
+    }
+
+    .mx_AddExistingToSpaceDialog_footer {
+        display: flex;
+        margin-top: 32px;
+
+        > span {
+            flex-grow: 1;
+            font-size: $font-12px;
+            line-height: $font-15px;
+
+            > * {
+                vertical-align: middle;
+            }
+        }
+
+        .mx_AccessibleButton {
+            display: inline-block;
+        }
+
+        .mx_AccessibleButton_kind_link {
+            padding: 0;
+        }
+    }
+
+    .mx_FormButton {
+        padding: 8px 22px;
+    }
+}
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 4159a38cfe..f1a8a4d71b 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -46,7 +46,7 @@ import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
 import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
 import {useStateArray} from "../../hooks/useStateArray";
 import SpacePublicShare from "../views/spaces/SpacePublicShare";
-import {showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
+import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
 import MemberAvatar from "../views/avatars/MemberAvatar";
 
 interface IProps {
@@ -124,6 +124,14 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
     let addRoomButtons;
     if (canAddRooms) {
         addRoomButtons = <React.Fragment>
+            <AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
+                const [added] = await showAddExistingRooms(cli, space);
+                if (added) {
+                    // TODO update rooms shown once we show hierarchy here
+                }
+            }}>
+                { _t("Add existing rooms & spaces") }
+            </AccessibleButton>
             <AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
                 showCreateNewRoom(cli, space);
             }}>
diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
new file mode 100644
index 0000000000..66efaefd9d
--- /dev/null
+++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
@@ -0,0 +1,208 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useState} from "react";
+import classNames from "classnames";
+import {Room} from "matrix-js-sdk/src/models/room";
+import {MatrixClient} from "matrix-js-sdk/src/client";
+
+import {_t} from '../../../languageHandler';
+import {IDialogProps} from "./IDialogProps";
+import BaseDialog from "./BaseDialog";
+import FormButton from "../elements/FormButton";
+import Dropdown from "../elements/Dropdown";
+import SearchBox from "../../structures/SearchBox";
+import SpaceStore from "../../../stores/SpaceStore";
+import RoomAvatar from "../avatars/RoomAvatar";
+import {getDisplayAliasForRoom} from "../../../Rooms";
+import AccessibleButton from "../elements/AccessibleButton";
+import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+import {allSettled} from "../../../utils/promise";
+import DMRoomMap from "../../../utils/DMRoomMap";
+import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
+import StyledCheckbox from "../elements/StyledCheckbox";
+
+interface IProps extends IDialogProps {
+    matrixClient: MatrixClient;
+    space: Room;
+    onCreateRoomClick(cli: MatrixClient, space: Room): void;
+}
+
+const Entry = ({ room, checked, onChange }) => {
+    return <div className="mx_AddExistingToSpaceDialog_entry">
+        <RoomAvatar room={room} height={32} width={32} />
+        <span className="mx_AddExistingToSpaceDialog_entry_name">{ room.name }</span>
+        <StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} />
+    </div>;
+};
+
+const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
+    const [query, setQuery] = useState("");
+    const lcQuery = query.toLowerCase();
+
+    const [selectedSpace, setSelectedSpace] = useState(space);
+    const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
+
+    const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
+    const existingSubspacesSet = new Set(existingSubspaces);
+    const spaces = SpaceStore.instance.getSpaces().filter(s => {
+        return !existingSubspacesSet.has(s) // not already in space
+            && space !== s // not the top-level space
+            && selectedSpace !== s // not the selected space
+            && s.name.toLowerCase().includes(lcQuery); // contains query
+    });
+
+    const existingRooms = SpaceStore.instance.getChildRooms(space.roomId);
+    const existingRoomsSet = new Set(existingRooms);
+    const rooms = cli.getVisibleRooms().filter(room => {
+        return !existingRoomsSet.has(room) // not already in space
+            && room.name.toLowerCase().includes(lcQuery) // contains query
+            && !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
+    });
+
+    const [busy, setBusy] = useState(false);
+    const [error, setError] = useState("");
+
+    let spaceOptionSection;
+    if (existingSubspacesSet.size > 0) {
+        const options = [space, ...existingSubspaces].map((space) => {
+            const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
+                mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
+            });
+            return <div key={space.roomId} className={classes}>
+                <RoomAvatar room={space} width={24} height={24} />
+                { space.name || getDisplayAliasForRoom(space) || space.roomId }
+            </div>;
+        });
+
+        spaceOptionSection = (
+            <Dropdown
+                id="mx_SpaceSelectDropdown"
+                onOptionChange={(key: string) => {
+                    setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
+                }}
+                value={selectedSpace.roomId}
+                label={_t("Space selection")}
+            >
+                { options }
+            </Dropdown>
+        );
+    } else {
+        spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
+            { space.name || getDisplayAliasForRoom(space) || space.roomId }
+        </div>;
+    }
+
+    const title = <React.Fragment>
+        <RoomAvatar room={selectedSpace} height={40} width={40} />
+        <div>
+            <h1>{ _t("Add existing spaces/rooms") }</h1>
+            { spaceOptionSection }
+        </div>
+    </React.Fragment>;
+
+    return <BaseDialog
+        title={title}
+        className="mx_AddExistingToSpaceDialog"
+        contentId="mx_AddExistingToSpaceDialog"
+        onFinished={onFinished}
+        fixedWidth={false}
+    >
+        { error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
+
+        <SearchBox
+            className="mx_textinput_icon mx_textinput_search"
+            placeholder={ _t("Filter your rooms and spaces") }
+            onSearch={setQuery}
+        />
+        <AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog">
+            { spaces.length > 0 ? (
+                <div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
+                    <h3>{ _t("Spaces") }</h3>
+                    { spaces.map(space => {
+                        return <Entry
+                            key={space.roomId}
+                            room={space}
+                            checked={selectedToAdd.has(space)}
+                            onChange={(checked) => {
+                                if (checked) {
+                                    selectedToAdd.add(space);
+                                } else {
+                                    selectedToAdd.delete(space);
+                                }
+                                setSelectedToAdd(new Set(selectedToAdd));
+                            }}
+                        />;
+                    }) }
+                </div>
+            ) : null }
+
+            { rooms.length > 0 ? (
+                <div className="mx_AddExistingToSpaceDialog_section">
+                    <h3>{ _t("Rooms") }</h3>
+                    { rooms.map(room => {
+                        return <Entry
+                            key={room.roomId}
+                            room={room}
+                            checked={selectedToAdd.has(room)}
+                            onChange={(checked) => {
+                                if (checked) {
+                                    selectedToAdd.add(room);
+                                } else {
+                                    selectedToAdd.delete(room);
+                                }
+                                setSelectedToAdd(new Set(selectedToAdd));
+                            }}
+                        />;
+                    }) }
+                </div>
+            ) : undefined }
+
+            { spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
+                { _t("No results") }
+            </span> : undefined }
+        </AutoHideScrollbar>
+
+        <div className="mx_AddExistingToSpaceDialog_footer">
+            <span>
+                <div>{ _t("Don't want to add an existing room?") }</div>
+                <AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
+                    { _t("Create a new room") }
+                </AccessibleButton>
+            </span>
+
+            <FormButton
+                label={busy ? _t("Applying...") : _t("Apply")}
+                disabled={busy || selectedToAdd.size < 1}
+                onClick={async () => {
+                    setBusy(true);
+                    try {
+                        await allSettled(Array.from(selectedToAdd).map((room) =>
+                            SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
+                        onFinished(true);
+                    } catch (e) {
+                        console.error("Failed to add rooms to space", e);
+                        setError(_t("Failed to add rooms to space"));
+                    }
+                    setBusy(false);
+                }}
+            />
+        </div>
+    </BaseDialog>;
+};
+
+export default AddExistingToSpaceDialog;
+
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index aeef76bf22..bb800b2af2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1978,6 +1978,15 @@
     "Add a new server...": "Add a new server...",
     "%(networkName)s rooms": "%(networkName)s rooms",
     "Matrix rooms": "Matrix rooms",
+    "Space selection": "Space selection",
+    "Add existing spaces/rooms": "Add existing spaces/rooms",
+    "Filter your rooms and spaces": "Filter your rooms and spaces",
+    "Spaces": "Spaces",
+    "Don't want to add an existing room?": "Don't want to add an existing room?",
+    "Create a new room": "Create a new room",
+    "Applying...": "Applying...",
+    "Apply": "Apply",
+    "Failed to add rooms to space": "Failed to add rooms to space",
     "Matrix ID": "Matrix ID",
     "Matrix Room ID": "Matrix Room ID",
     "email address": "email address",
@@ -2570,7 +2579,7 @@
     "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
     "Accept Invite": "Accept Invite",
     "Invite people": "Invite people",
-    "Create a new room": "Create a new room",
+    "Add existing rooms & spaces": "Add existing rooms & spaces",
     "%(count)s members|other": "%(count)s members",
     "%(count)s members|one": "%(count)s member",
     "<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",
diff --git a/src/utils/space.ts b/src/utils/space.ts
index c995b860ee..bc31829f45 100644
--- a/src/utils/space.ts
+++ b/src/utils/space.ts
@@ -21,6 +21,7 @@ import {EventType} from "matrix-js-sdk/src/@types/event";
 import {calculateRoomVia} from "../utils/permalinks/Permalinks";
 import Modal from "../Modal";
 import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog";
+import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog";
 import CreateRoomDialog from "../components/views/dialogs/CreateRoomDialog";
 import createRoom, {IOpts} from "../createRoom";
 
@@ -49,6 +50,20 @@ export const showSpaceSettings = (cli: MatrixClient, space: Room) => {
     }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
 };
 
+export const showAddExistingRooms = async (cli: MatrixClient, space: Room) => {
+    return Modal.createTrackedDialog(
+        "Space Landing",
+        "Add Existing",
+        AddExistingToSpaceDialog,
+        {
+            matrixClient: cli,
+            onCreateRoomClick: showCreateNewRoom,
+            space,
+        },
+        "mx_AddExistingToSpaceDialog_wrapper",
+    ).finished;
+};
+
 export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
     const modal = Modal.createTrackedDialog<[boolean, IOpts]>(
         "Space Landing",

From 4e93452275ed7ed8224d14a62c8711a85fdc6e6f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 14:02:03 +0000
Subject: [PATCH 252/389] Tweak resizer collapse distributor behaviour to work
 with the expanding space panel

---
 src/components/structures/LoggedInView.tsx | 12 +++++++++++-
 src/resizer/distributors/collapse.ts       |  9 ++++++++-
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 1694b4bcf5..4e768bd9e5 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -223,7 +223,14 @@ class LoggedInView extends React.Component<IProps, IState> {
         let size;
         let collapsed;
         const collapseConfig: ICollapseConfig = {
-            toggleSize: 260 - 50,
+            // TODO: the space panel currently does not have a fixed width,
+            // just the headers at each level have a max-width of 150px
+            // Taking 222px for the space panel for now,
+            // so this will look slightly off for now,
+            // depending on the depth of your space tree.
+            // To fix this, we'll need to turn toggleSize
+            // into a callback so it can be measured when starting the resize operation
+            toggleSize: 222 + 68,
             onCollapsed: (_collapsed) => {
                 collapsed = _collapsed;
                 if (_collapsed) {
@@ -244,6 +251,9 @@ class LoggedInView extends React.Component<IProps, IState> {
                 if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
                 this.props.resizeNotifier.stopResizing();
             },
+            isItemCollapsed: domNode => {
+                return domNode.classList.contains("mx_LeftPanel_minimized");
+            },
         };
         const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig);
         resizer.setClassNames({
diff --git a/src/resizer/distributors/collapse.ts b/src/resizer/distributors/collapse.ts
index ddf3bd687e..f8db0be52c 100644
--- a/src/resizer/distributors/collapse.ts
+++ b/src/resizer/distributors/collapse.ts
@@ -22,6 +22,7 @@ import Sizer from "../sizer";
 export interface ICollapseConfig extends IConfig {
     toggleSize: number;
     onCollapsed?(collapsed: boolean, id: string, element: HTMLElement): void;
+    isItemCollapsed(element: HTMLElement): boolean;
 }
 
 class CollapseItem extends ResizeItem<ICollapseConfig> {
@@ -31,6 +32,11 @@ class CollapseItem extends ResizeItem<ICollapseConfig> {
             callback(collapsed, this.id, this.domNode);
         }
     }
+
+    get isCollapsed() {
+        const isItemCollapsed = this.resizer.config.isItemCollapsed;
+        return isItemCollapsed(this.domNode);
+    }
 }
 
 export default class CollapseDistributor extends FixedDistributor<ICollapseConfig, CollapseItem> {
@@ -39,11 +45,12 @@ export default class CollapseDistributor extends FixedDistributor<ICollapseConfi
     }
 
     private readonly toggleSize: number;
-    private isCollapsed = false;
+    private isCollapsed: boolean;
 
     constructor(item: CollapseItem) {
         super(item);
         this.toggleSize = item.resizer?.config?.toggleSize;
+        this.isCollapsed = item.isCollapsed;
     }
 
     public resize(newSize: number) {

From faf7a4b8bce57d7d2654b49b9413762e1e876daf Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 14:11:38 +0000
Subject: [PATCH 253/389] Initial Space room directory view

---
 res/css/_components.scss                      |   1 +
 res/css/structures/_SpaceRoomDirectory.scss   | 231 +++++++
 src/components/structures/MatrixChat.tsx      |  17 +-
 .../structures/SpaceRoomDirectory.tsx         | 572 ++++++++++++++++++
 src/i18n/strings/en_EN.json                   |  10 +
 5 files changed, 827 insertions(+), 4 deletions(-)
 create mode 100644 res/css/structures/_SpaceRoomDirectory.scss
 create mode 100644 src/components/structures/SpaceRoomDirectory.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index 8569f62de9..daa7016623 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -28,6 +28,7 @@
 @import "./structures/_ScrollPanel.scss";
 @import "./structures/_SearchBox.scss";
 @import "./structures/_SpacePanel.scss";
+@import "./structures/_SpaceRoomDirectory.scss";
 @import "./structures/_SpaceRoomView.scss";
 @import "./structures/_TabbedView.scss";
 @import "./structures/_ToastContainer.scss";
diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss
new file mode 100644
index 0000000000..5cb91820cf
--- /dev/null
+++ b/res/css/structures/_SpaceRoomDirectory.scss
@@ -0,0 +1,231 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_SpaceRoomDirectory_dialogWrapper > .mx_Dialog {
+    max-width: 960px;
+    height: 100%;
+}
+
+.mx_SpaceRoomDirectory {
+    height: 100%;
+    margin-bottom: 12px;
+    color: $primary-fg-color;
+    word-break: break-word;
+    display: flex;
+    flex-direction: column;
+
+    .mx_Dialog_title {
+        display: flex;
+
+        .mx_BaseAvatar {
+            margin-right: 16px;
+        }
+
+        .mx_BaseAvatar_image {
+            border-radius: 8px;
+        }
+
+        > div {
+            > h1 {
+                font-weight: $font-semi-bold;
+                font-size: $font-18px;
+                line-height: $font-22px;
+                margin: 0;
+            }
+
+            > div {
+                color: $secondary-fg-color;
+                font-size: $font-15px;
+                line-height: $font-24px;
+            }
+        }
+    }
+
+    .mx_Dialog_content {
+        // TODO fix scrollbar
+        //display: flex;
+        //flex-direction: column;
+        //height: calc(100% - 80px);
+
+        .mx_AccessibleButton_kind_link {
+            padding: 0;
+        }
+
+        .mx_SearchBox {
+            margin: 24px 0 28px;
+        }
+
+        .mx_SpaceRoomDirectory_listHeader {
+            display: flex;
+            font-size: $font-12px;
+            line-height: $font-15px;
+            color: $secondary-fg-color;
+
+            .mx_FormButton {
+                margin-bottom: 8px;
+            }
+
+            > span {
+                margin: auto 0 0 auto;
+            }
+        }
+    }
+}
+
+.mx_SpaceRoomDirectory_list {
+    margin-top: 8px;
+
+    .mx_SpaceRoomDirectory_roomCount {
+        > h3 {
+            display: inline;
+            font-weight: $font-semi-bold;
+            font-size: $font-18px;
+            line-height: $font-22px;
+            color: $primary-fg-color;
+        }
+
+        > span {
+            margin-left: 8px;
+            font-size: $font-15px;
+            line-height: $font-24px;
+            color: $secondary-fg-color;
+        }
+    }
+
+    .mx_SpaceRoomDirectory_subspace {
+        margin-top: 8px;
+
+        .mx_SpaceRoomDirectory_subspace_info {
+            display: flex;
+            flex-direction: row;
+            align-items: center;
+            margin-bottom: 8px;
+            color: $secondary-fg-color;
+            font-weight: $font-semi-bold;
+            font-size: $font-12px;
+            line-height: $font-15px;
+
+            .mx_BaseAvatar {
+                margin-right: 12px;
+                vertical-align: middle;
+            }
+
+            .mx_BaseAvatar_image {
+                border-radius: 8px;
+            }
+
+            .mx_SpaceRoomDirectory_actions {
+                text-align: right;
+                height: min-content;
+                margin-left: auto;
+                margin-right: 16px;
+            }
+        }
+
+        .mx_SpaceRoomDirectory_subspace_children {
+            margin-left: 12px;
+            border-left: 2px solid $space-button-outline-color;
+            padding-left: 24px;
+        }
+    }
+
+    .mx_SpaceRoomDirectory_roomTile {
+        padding: 16px;
+        border-radius: 8px;
+        border: 1px solid $space-button-outline-color;
+        margin: 8px 0 16px;
+        display: flex;
+        min-height: 76px;
+        box-sizing: border-box;
+
+        &.mx_AccessibleButton:hover {
+            background-color: rgba(141, 151, 165, 0.1);
+        }
+
+        .mx_BaseAvatar {
+            margin-right: 16px;
+            margin-top: 6px;
+        }
+
+        .mx_SpaceRoomDirectory_roomTile_info {
+            display: inline-block;
+            font-size: $font-15px;
+            flex-grow: 1;
+            height: min-content;
+            margin: auto 0;
+
+            .mx_SpaceRoomDirectory_roomTile_name {
+                font-weight: $font-semi-bold;
+                line-height: $font-18px;
+            }
+            .mx_SpaceRoomDirectory_roomTile_topic {
+                line-height: $font-24px;
+                color: $secondary-fg-color;
+            }
+        }
+
+        .mx_SpaceRoomDirectory_roomTile_memberCount {
+            position: relative;
+            margin: auto 0 auto 24px;
+            padding: 0 0 0 28px;
+            line-height: $font-24px;
+            display: inline-block;
+            width: 32px;
+
+            &::before {
+                position: absolute;
+                content: '';
+                width: 24px;
+                height: 24px;
+                top: 0;
+                left: 0;
+                mask-position: center;
+                mask-repeat: no-repeat;
+                mask-size: contain;
+                background-color: $secondary-fg-color;
+                mask-image: url('$(res)/img/element-icons/community-members.svg');
+            }
+        }
+
+        .mx_SpaceRoomDirectory_actions {
+            width: 180px;
+            text-align: right;
+            height: min-content;
+            margin: auto 0 auto 28px;
+
+            .mx_AccessibleButton {
+                vertical-align: middle;
+
+                & + .mx_AccessibleButton {
+                    margin-left: 24px;
+                }
+            }
+        }
+    }
+
+    .mx_SpaceRoomDirectory_actions {
+        .mx_SpaceRoomDirectory_actionsText {
+            font-weight: normal;
+            font-size: $font-12px;
+            line-height: $font-15px;
+            color: $secondary-fg-color;
+        }
+
+        .mx_Checkbox {
+            display: inline-block;
+        }
+    }
+}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 83b3565738..1700b627db 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -82,6 +82,8 @@ import {UIFeature} from "../../settings/UIFeature";
 import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
 import DialPadModal from "../views/voip/DialPadModal";
 import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
+import SpaceStore from "../../stores/SpaceStore";
+import SpaceRoomDirectory from "./SpaceRoomDirectory";
 
 /** constants for MatrixChat.state.view */
 export enum Views {
@@ -691,10 +693,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                 break;
             }
             case Action.ViewRoomDirectory: {
-                const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
-                Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
-                    initialText: payload.initialText,
-                }, 'mx_RoomDirectory_dialogWrapper', false, true);
+                if (SpaceStore.instance.activeSpace) {
+                    Modal.createTrackedDialog("Space room directory", "", SpaceRoomDirectory, {
+                        space: SpaceStore.instance.activeSpace,
+                        initialText: payload.initialText,
+                    }, "mx_SpaceRoomDirectory_dialogWrapper", false, true);
+                } else {
+                    const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
+                    Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
+                        initialText: payload.initialText,
+                    }, 'mx_RoomDirectory_dialogWrapper', false, true);
+                }
 
                 // View the welcome or home page if we need something to look at
                 this.viewSomethingBehindModal();
diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
new file mode 100644
index 0000000000..7f7b9dbb99
--- /dev/null
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -0,0 +1,572 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useMemo, useRef, useState} from "react";
+import Room from "matrix-js-sdk/src/models/room";
+import MatrixEvent from "matrix-js-sdk/src/models/event";
+import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
+
+import {MatrixClientPeg} from "../../MatrixClientPeg";
+import dis from "../../dispatcher/dispatcher";
+import {_t} from "../../languageHandler";
+import AccessibleButton from "../views/elements/AccessibleButton";
+import BaseDialog from "../views/dialogs/BaseDialog";
+import FormButton from "../views/elements/FormButton";
+import SearchBox from "./SearchBox";
+import RoomAvatar from "../views/avatars/RoomAvatar";
+import RoomName from "../views/elements/RoomName";
+import {useAsyncMemo} from "../../hooks/useAsyncMemo";
+import {shouldShowSpaceSettings} from "../../utils/space";
+import {EnhancedMap} from "../../utils/maps";
+import StyledCheckbox from "../views/elements/StyledCheckbox";
+import AutoHideScrollbar from "./AutoHideScrollbar";
+import BaseAvatar from "../views/avatars/BaseAvatar";
+
+interface IProps {
+    space: Room;
+    initialText?: string;
+    onFinished(): void;
+}
+
+/* eslint-disable camelcase */
+export interface ISpaceSummaryRoom {
+    canonical_alias?: string;
+    aliases: string[];
+    avatar_url?: string;
+    guest_can_join: boolean;
+    name?: string;
+    num_joined_members: number
+    room_id: string;
+    topic?: string;
+    world_readable: boolean;
+    num_refs: number;
+    room_type: string;
+}
+
+export interface ISpaceSummaryEvent {
+    room_id: string;
+    event_id: string;
+    origin_server_ts: number;
+    type: string;
+    state_key: string;
+    content: {
+        order?: string;
+        auto_join?: boolean;
+        via?: string;
+    };
+}
+/* eslint-enable camelcase */
+
+interface ISubspaceProps {
+    space: ISpaceSummaryRoom;
+    event?: MatrixEvent;
+    editing?: boolean;
+    onPreviewClick?(): void;
+    queueAction?(action: IAction): void;
+    onJoinClick?(): void;
+}
+
+const SubSpace: React.FC<ISubspaceProps> = ({
+    space,
+    editing,
+    event,
+    queueAction,
+    onJoinClick,
+    onPreviewClick,
+    children,
+}) => {
+    const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
+
+    const evContent = event?.getContent();
+    const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
+    const [removed, _setRemoved] = useState(!evContent?.via);
+
+    const cli = MatrixClientPeg.get();
+    const cliRoom = cli.getRoom(space.room_id);
+    const myMembership = cliRoom?.getMyMembership();
+
+    // TODO DRY code
+    let actions;
+    if (editing && queueAction) {
+        if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
+            const setAutoJoin = () => {
+                _setAutoJoin(v => {
+                    queueAction({
+                        event,
+                        removed,
+                        autoJoin: !v,
+                    });
+                    return !v;
+                });
+            };
+
+            const setRemoved = () => {
+                _setRemoved(v => {
+                    queueAction({
+                        event,
+                        removed: !v,
+                        autoJoin,
+                    });
+                    return !v;
+                });
+            };
+
+            if (removed) {
+                actions = <React.Fragment>
+                    <FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
+                </React.Fragment>;
+            } else {
+                actions = <React.Fragment>
+                    <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
+                    <StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
+                </React.Fragment>;
+            }
+        } else {
+            actions = <span className="mx_SpaceRoomDirectory_actionsText">
+                { _t("No permissions")}
+            </span>;
+        }
+        // TODO confirm remove from space click behaviour here
+    } else {
+        if (myMembership === "join") {
+            actions = <span className="mx_SpaceRoomDirectory_actionsText">
+                { _t("You're in this space")}
+            </span>;
+        } else if (onJoinClick) {
+            actions = <React.Fragment>
+                <AccessibleButton onClick={onPreviewClick} kind="link">
+                    { _t("Preview") }
+                </AccessibleButton>
+                <FormButton onClick={onJoinClick} label={_t("Join")} />
+            </React.Fragment>
+        }
+    }
+
+    let url: string;
+    if (space.avatar_url) {
+        url = MatrixClientPeg.get().mxcUrlToHttp(space.avatar_url,
+            Math.floor(24 * window.devicePixelRatio),
+            Math.floor(24 * window.devicePixelRatio),
+            "crop");
+    }
+
+    return <div className="mx_SpaceRoomDirectory_subspace">
+        <div className="mx_SpaceRoomDirectory_subspace_info">
+            <BaseAvatar name={name} idName={space.room_id} url={url} width={24} height={24} />
+            { name }
+
+            <div className="mx_SpaceRoomDirectory_actions">
+                { actions }
+            </div>
+        </div>
+        <div className="mx_SpaceRoomDirectory_subspace_children">
+            { children }
+        </div>
+    </div>
+};
+
+interface IAction {
+    event: MatrixEvent;
+    removed: boolean;
+    autoJoin: boolean;
+}
+
+interface IRoomTileProps {
+    room: ISpaceSummaryRoom;
+    event?: MatrixEvent;
+    editing?: boolean;
+    onPreviewClick(): void;
+    queueAction?(action: IAction): void;
+    onJoinClick?(): void;
+}
+
+const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinClick }: IRoomTileProps) => {
+    const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
+
+    const evContent = event?.getContent();
+    const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
+    const [removed, _setRemoved] = useState(!evContent?.via);
+
+    const cli = MatrixClientPeg.get();
+    const cliRoom = cli.getRoom(room.room_id);
+    const myMembership = cliRoom?.getMyMembership();
+
+    let actions;
+    if (editing && queueAction) {
+        if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
+            const setAutoJoin = () => {
+                _setAutoJoin(v => {
+                    queueAction({
+                        event,
+                        removed,
+                        autoJoin: !v,
+                    });
+                    return !v;
+                });
+            };
+
+            const setRemoved = () => {
+                _setRemoved(v => {
+                    queueAction({
+                        event,
+                        removed: !v,
+                        autoJoin,
+                    });
+                    return !v;
+                });
+            };
+
+            if (removed) {
+                actions = <React.Fragment>
+                    <FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
+                </React.Fragment>;
+            } else {
+                actions = <React.Fragment>
+                    <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
+                    <StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
+                </React.Fragment>;
+            }
+        } else {
+            actions = <span className="mx_SpaceRoomDirectory_actionsText">
+                { _t("No permissions")}
+            </span>;
+        }
+        // TODO confirm remove from space click behaviour here
+    } else {
+        if (myMembership === "join") {
+            actions = <span className="mx_SpaceRoomDirectory_actionsText">
+                { _t("You're in this room")}
+            </span>;
+        } else if (onJoinClick) {
+            actions = <React.Fragment>
+                <AccessibleButton onClick={onPreviewClick} kind="link">
+                    { _t("Preview") }
+                </AccessibleButton>
+                <FormButton onClick={onJoinClick} label={_t("Join")} />
+            </React.Fragment>
+        }
+    }
+
+    let url: string;
+    if (room.avatar_url) {
+        url = cli.mxcUrlToHttp(room.avatar_url,
+            Math.floor(32 * window.devicePixelRatio),
+            Math.floor(32 * window.devicePixelRatio),
+            "crop");
+    }
+
+    const content = <React.Fragment>
+        <BaseAvatar name={name} idName={room.room_id} url={url} width={32} height={32} />
+
+        <div className="mx_SpaceRoomDirectory_roomTile_info">
+            <div className="mx_SpaceRoomDirectory_roomTile_name">
+                { name }
+            </div>
+            <div className="mx_SpaceRoomDirectory_roomTile_topic">
+                { room.topic }
+            </div>
+        </div>
+        <div className="mx_SpaceRoomDirectory_roomTile_memberCount">
+            { room.num_joined_members }
+        </div>
+
+        <div className="mx_SpaceRoomDirectory_actions">
+            { actions }
+        </div>
+    </React.Fragment>;
+
+    if (editing) {
+        return <div className="mx_SpaceRoomDirectory_roomTile">
+            { content }
+        </div>
+    }
+
+    return <AccessibleButton className="mx_SpaceRoomDirectory_roomTile" onClick={onPreviewClick}>
+        { content }
+    </AccessibleButton>;
+};
+
+export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
+    // Don't let the user view a room they won't be able to either peek or join:
+    // fail earlier so they don't have to click back to the directory.
+    if (MatrixClientPeg.get().isGuest()) {
+        if (!room.world_readable && !room.guest_can_join) {
+            dis.dispatch({ action: "require_registration" });
+            return;
+        }
+    }
+
+    const roomAlias = getDisplayAliasForRoom(room) || undefined;
+    dis.dispatch({
+        action: "view_room",
+        auto_join: autoJoin,
+        should_peek: true,
+        _type: "room_directory", // instrumentation
+        room_alias: roomAlias,
+        room_id: room.room_id,
+        via_servers: viaServers,
+        oob_data: {
+            avatarUrl: room.avatar_url,
+            // XXX: This logic is duplicated from the JS SDK which would normally decide what the name is.
+            name: room.name || roomAlias || _t("Unnamed room"),
+        },
+    });
+};
+
+interface IHierarchyLevelProps {
+    spaceId: string;
+    rooms: Map<string, ISpaceSummaryRoom>;
+    editing?: boolean;
+    relations: EnhancedMap<string, string[]>;
+    parents: Set<string>;
+    queueAction?(action: IAction): void;
+    onPreviewClick(roomId: string): void;
+    onRemoveFromSpaceClick?(roomId: string): void;
+    onJoinClick?(roomId: string): void;
+}
+
+export const HierarchyLevel = ({
+    spaceId,
+    rooms,
+    editing,
+    relations,
+    parents,
+    onPreviewClick,
+    onJoinClick,
+    queueAction,
+}: IHierarchyLevelProps) => {
+    const cli = MatrixClientPeg.get();
+    const space = cli.getRoom(spaceId);
+    // TODO respect order
+    const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => {
+        if (!rooms.has(roomId)) return result; // TODO wat
+        result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId);
+        return result;
+    }, [[], []]) || [[], []];
+
+    // Don't render this subspace if it has no rooms we can show
+    // TODO this is broken - as a space may have subspaces we still need to show
+    // if (!childRooms.length) return null;
+
+    const userId = cli.getUserId();
+
+    const newParents = new Set(parents).add(spaceId);
+    return <React.Fragment>
+        {
+            childRooms.map(roomId => (
+                <RoomTile
+                    key={roomId}
+                    room={rooms.get(roomId)}
+                    event={space?.currentState.maySendStateEvent(EventType.SpaceChild, userId)
+                        ? space?.currentState.getStateEvents(EventType.SpaceChild, roomId)
+                        : undefined}
+                    editing={editing}
+                    queueAction={queueAction}
+                    onPreviewClick={() => {
+                        onPreviewClick(roomId);
+                    }}
+                    onJoinClick={onJoinClick ? () => {
+                        onJoinClick(roomId);
+                    } : undefined}
+                />
+            ))
+        }
+
+        {
+            subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => (
+                <SubSpace
+                    key={roomId}
+                    space={rooms.get(roomId)}
+                    event={space?.currentState.getStateEvents(EventType.SpaceChild, roomId)}
+                    editing={editing}
+                    queueAction={queueAction}
+                    onPreviewClick={() => {
+                        onPreviewClick(roomId);
+                    }}
+                    onJoinClick={() => {
+                        onJoinClick(roomId);
+                    }}
+                >
+                    <HierarchyLevel
+                        spaceId={roomId}
+                        rooms={rooms}
+                        editing={editing}
+                        relations={relations}
+                        parents={newParents}
+                        onPreviewClick={onPreviewClick}
+                        onJoinClick={onJoinClick}
+                        queueAction={queueAction}
+                    />
+                </SubSpace>
+            ))
+        }
+    </React.Fragment>
+};
+
+const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
+    // TODO pagination
+    const cli = MatrixClientPeg.get();
+    const [query, setQuery] = useState(initialText);
+    const [isEditing, setIsEditing] = useState(false);
+
+    const onCreateRoomClick = () => {
+        dis.dispatch({
+            action: 'view_create_room',
+            public: true,
+        });
+        onFinished();
+    };
+
+    // stored within a ref as we don't need to re-render when it changes
+    const pendingActions = useRef(new Map<string, IAction>());
+
+    let adminButton;
+    if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test
+        const onManageButtonClicked = () => {
+            setIsEditing(true);
+        };
+
+        const onSaveButtonClicked = () => {
+            // TODO setBusy
+            pendingActions.current.forEach(({event, autoJoin, removed}) => {
+                const content = {
+                    ...event.getContent(),
+                    auto_join: autoJoin,
+                };
+
+                if (removed) {
+                    delete content["via"];
+                }
+
+                cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey());
+            });
+            setIsEditing(false);
+        };
+
+        if (isEditing) {
+            adminButton = <React.Fragment>
+                <FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
+                <span>{ _t("All users join by default") }</span>
+            </React.Fragment>;
+        } else {
+            adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;
+        }
+    }
+
+    const [rooms, relations, viaMap] = useAsyncMemo(async () => {
+        try {
+            const data = await cli.getSpaceSummary(space.roomId);
+
+            const parentChildRelations = new EnhancedMap<string, string[]>();
+            const viaMap = new EnhancedMap<string, Set<string>>();
+            data.events.map((ev: ISpaceSummaryEvent) => {
+                if (ev.type === EventType.SpaceChild) {
+                    parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
+                }
+                if (Array.isArray(ev.content["via"])) {
+                    const set = viaMap.getOrCreate(ev.state_key, new Set());
+                    ev.content["via"].forEach(via => set.add(via));
+                }
+            });
+
+            return [data.rooms, parentChildRelations, viaMap];
+        } catch (e) {
+            console.error(e); // TODO
+        }
+
+        return [];
+    }, [space], []);
+
+    const roomsMap = useMemo(() => {
+        if (!rooms) return null;
+        const lcQuery = query.toLowerCase();
+
+        const filteredRooms = rooms.filter(r => {
+            return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms
+                || r.name?.toLowerCase().includes(lcQuery)
+                || r.topic?.toLowerCase().includes(lcQuery);
+        });
+
+        return new Map<string, ISpaceSummaryRoom>(filteredRooms.map(r => [r.room_id, r]));
+        // const root = rooms.get(space.roomId);
+    }, [rooms, query]);
+
+    const title = <React.Fragment>
+        <RoomAvatar room={space} height={40} width={40} />
+        <div>
+            <h1>{ _t("Explore rooms") }</h1>
+            <div><RoomName room={space} /></div>
+        </div>
+    </React.Fragment>;
+    const explanation =
+        _t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
+            {a: sub => {
+                return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
+            }},
+        );
+
+    let content;
+    if (roomsMap) {
+        content = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
+            <HierarchyLevel
+                spaceId={space.roomId}
+                rooms={roomsMap}
+                editing={isEditing}
+                relations={relations}
+                parents={new Set()}
+                queueAction={action => {
+                    pendingActions.current.set(action.event.room_id, action);
+                }}
+                onPreviewClick={roomId => {
+                    showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false);
+                    onFinished();
+                }}
+                onJoinClick={(roomId) => {
+                    showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true);
+                    onFinished();
+                }}
+            />
+        </AutoHideScrollbar>;
+    }
+
+    // TODO loading state/error state
+    return (
+        <BaseDialog className="mx_SpaceRoomDirectory" hasCancel={true} onFinished={onFinished} title={title}>
+            <div className="mx_Dialog_content">
+                { explanation }
+
+                <SearchBox
+                    className="mx_textinput_icon mx_textinput_search"
+                    placeholder={ _t("Find a room...") }
+                    onSearch={setQuery}
+                />
+
+                <div className="mx_SpaceRoomDirectory_listHeader">
+                    { adminButton }
+                </div>
+                { content }
+            </div>
+        </BaseDialog>
+    );
+};
+
+export default SpaceRoomDirectory;
+
+// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
+// but works with the objects we get from the public room list
+function getDisplayAliasForRoom(room: ISpaceSummaryRoom) {
+    return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index bb800b2af2..04a6d63272 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2577,6 +2577,16 @@
     "Failed to reject invite": "Failed to reject invite",
     "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
     "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
+    "Unnamed Space": "Unnamed Space",
+    "Undo": "Undo",
+    "Remove from Space": "Remove from Space",
+    "No permissions": "No permissions",
+    "You're in this space": "You're in this space",
+    "You're in this room": "You're in this room",
+    "Save changes": "Save changes",
+    "All users join by default": "All users join by default",
+    "Manage rooms": "Manage rooms",
+    "Find a room...": "Find a room...",
     "Accept Invite": "Accept Invite",
     "Invite people": "Invite people",
     "Add existing rooms & spaces": "Add existing rooms & spaces",

From ca1bd78921e0fb05bbaaae185f3a29402b61a38e Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 14:19:40 +0000
Subject: [PATCH 254/389] Add space specific variant of the dropdown on "Rooms
 +" sublist

---
 res/css/views/rooms/_RoomList.scss            |  5 +-
 .../element-icons/roomlist/hash-circle.svg    |  7 +++
 .../element-icons/roomlist/plus-circle.svg    |  3 ++
 src/components/views/rooms/RoomList.tsx       | 47 +++++++++++++++++++
 src/i18n/strings/en_EN.json                   |  4 ++
 5 files changed, 65 insertions(+), 1 deletion(-)
 create mode 100644 res/img/element-icons/roomlist/hash-circle.svg
 create mode 100644 res/img/element-icons/roomlist/plus-circle.svg

diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss
index 66e1b827d0..d49ed4b736 100644
--- a/res/css/views/rooms/_RoomList.scss
+++ b/res/css/views/rooms/_RoomList.scss
@@ -19,7 +19,10 @@ limitations under the License.
 }
 
 .mx_RoomList_iconPlus::before {
-    mask-image: url('$(res)/img/element-icons/roomlist/plus.svg');
+    mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg');
+}
+.mx_RoomList_iconHash::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg');
 }
 .mx_RoomList_iconExplore::before {
     mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
diff --git a/res/img/element-icons/roomlist/hash-circle.svg b/res/img/element-icons/roomlist/hash-circle.svg
new file mode 100644
index 0000000000..924b22cf32
--- /dev/null
+++ b/res/img/element-icons/roomlist/hash-circle.svg
@@ -0,0 +1,7 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <mask id="path-1-inside-1" fill="white">
+        <path fill-rule="evenodd" clip-rule="evenodd" d="M9 17C13.4183 17 17 13.4183 17 9C17 4.58172 13.4183 1 9 1C4.58172 1 1 4.58172 1 9C1 13.4183 4.58172 17 9 17ZM8.33969 5.15616C8.38275 4.74419 8.0837 4.37532 7.67173 4.33225C7.25976 4.28918 6.89088 4.58824 6.84781 5.00021L6.68776 6.53131H5.65625C5.24204 6.53131 4.90625 6.8671 4.90625 7.28131C4.90625 7.69552 5.24204 8.03131 5.65625 8.03131H6.53095L6.32841 9.96881H5.39062C4.97641 9.96881 4.64062 10.3046 4.64062 10.7188C4.64062 11.133 4.97641 11.4688 5.39062 11.4688H6.17161L6.01969 12.9221C5.97662 13.3341 6.27568 13.7029 6.68765 13.746C7.09962 13.7891 7.46849 13.49 7.51156 13.078L7.67978 11.4688H9.49973L9.34781 12.9221C9.30475 13.3341 9.6038 13.7029 10.0158 13.746C10.4277 13.7891 10.7966 13.49 10.8397 13.078L11.0079 11.4688H12.2969C12.7111 11.4688 13.0469 11.133 13.0469 10.7188C13.0469 10.3046 12.7111 9.96881 12.2969 9.96881H11.1647L11.3673 8.03131H12.2969C12.7111 8.03131 13.0469 7.69552 13.0469 7.28131C13.0469 6.8671 12.7111 6.53131 12.2969 6.53131H11.5241L11.6678 5.15616C11.7109 4.74419 11.4118 4.37532 10.9999 4.33225C10.5879 4.28918 10.219 4.58824 10.1759 5.00021L10.0159 6.53131H8.19593L8.33969 5.15616ZM9.65654 9.96881H7.83659L8.03913 8.03131H9.85908L9.65654 9.96881Z"/>
+    </mask>
+    <path fill-rule="evenodd" clip-rule="evenodd" d="M9 17C13.4183 17 17 13.4183 17 9C17 4.58172 13.4183 1 9 1C4.58172 1 1 4.58172 1 9C1 13.4183 4.58172 17 9 17ZM8.33969 5.15616C8.38275 4.74419 8.0837 4.37532 7.67173 4.33225C7.25976 4.28918 6.89088 4.58824 6.84781 5.00021L6.68776 6.53131H5.65625C5.24204 6.53131 4.90625 6.8671 4.90625 7.28131C4.90625 7.69552 5.24204 8.03131 5.65625 8.03131H6.53095L6.32841 9.96881H5.39062C4.97641 9.96881 4.64062 10.3046 4.64062 10.7188C4.64062 11.133 4.97641 11.4688 5.39062 11.4688H6.17161L6.01969 12.9221C5.97662 13.3341 6.27568 13.7029 6.68765 13.746C7.09962 13.7891 7.46849 13.49 7.51156 13.078L7.67978 11.4688H9.49973L9.34781 12.9221C9.30475 13.3341 9.6038 13.7029 10.0158 13.746C10.4277 13.7891 10.7966 13.49 10.8397 13.078L11.0079 11.4688H12.2969C12.7111 11.4688 13.0469 11.133 13.0469 10.7188C13.0469 10.3046 12.7111 9.96881 12.2969 9.96881H11.1647L11.3673 8.03131H12.2969C12.7111 8.03131 13.0469 7.69552 13.0469 7.28131C13.0469 6.8671 12.7111 6.53131 12.2969 6.53131H11.5241L11.6678 5.15616C11.7109 4.74419 11.4118 4.37532 10.9999 4.33225C10.5879 4.28918 10.219 4.58824 10.1759 5.00021L10.0159 6.53131H8.19593L8.33969 5.15616ZM9.65654 9.96881H7.83659L8.03913 8.03131H9.85908L9.65654 9.96881Z" fill="black"/>
+    <path d="M7.67173 4.33225L7.5331 5.65836H7.5331L7.67173 4.33225ZM8.33969 5.15616L9.66579 5.29479V5.29479L8.33969 5.15616ZM6.84781 5.00021L5.52171 4.86158L5.52171 4.86158L6.84781 5.00021ZM6.68776 6.53131V7.86464H7.88898L8.01387 6.66994L6.68776 6.53131ZM6.53095 8.03131L7.85706 8.16994L8.01093 6.69798H6.53095V8.03131ZM6.32841 9.96881V11.3021H7.52963L7.65452 10.1074L6.32841 9.96881ZM6.17161 11.4688L7.49772 11.6074L7.65159 10.1355H6.17161V11.4688ZM6.01969 12.9221L4.69358 12.7835V12.7835L6.01969 12.9221ZM6.68765 13.746L6.82628 12.4199H6.82627L6.68765 13.746ZM7.51156 13.078L8.83767 13.2167L8.83767 13.2167L7.51156 13.078ZM7.67978 11.4688V10.1355H6.47857L6.35368 11.3302L7.67978 11.4688ZM9.49973 11.4688L10.8258 11.6074L10.9797 10.1355H9.49973V11.4688ZM9.34781 12.9221L10.6739 13.0607V13.0607L9.34781 12.9221ZM10.0158 13.746L10.1544 12.4199H10.1544L10.0158 13.746ZM10.8397 13.078L12.1658 13.2167V13.2167L10.8397 13.078ZM11.0079 11.4688V10.1355H9.80669L9.6818 11.3302L11.0079 11.4688ZM11.1647 9.96881L9.83861 9.83018L9.68473 11.3021H11.1647V9.96881ZM11.3673 8.03131V6.69798H10.166L10.0411 7.89268L11.3673 8.03131ZM11.5241 6.53131L10.198 6.39268L10.0441 7.86464H11.5241V6.53131ZM11.6678 5.15616L10.3417 5.01754V5.01754L11.6678 5.15616ZM10.9999 4.33225L11.1385 3.00614H11.1385L10.9999 4.33225ZM10.1759 5.00021L8.84983 4.86158L8.84983 4.86158L10.1759 5.00021ZM10.0159 6.53131V7.86464H11.2171L11.342 6.66994L10.0159 6.53131ZM8.19593 6.53131L6.86982 6.39268L6.71595 7.86464H8.19593V6.53131ZM7.83659 9.96881L6.51048 9.83018L6.35661 11.3021H7.83659V9.96881ZM9.65654 9.96881V11.3021H10.8578L10.9826 10.1074L9.65654 9.96881ZM8.03913 8.03131V6.69798H6.83791L6.71302 7.89268L8.03913 8.03131ZM9.85908 8.03131L11.1852 8.16994L11.3391 6.69798H9.85908V8.03131ZM15.6667 9C15.6667 12.6819 12.6819 15.6667 9 15.6667V18.3333C14.1547 18.3333 18.3333 14.1547 18.3333 9H15.6667ZM9 2.33333C12.6819 2.33333 15.6667 5.3181 15.6667 9H18.3333C18.3333 3.84534 14.1547 -0.333333 9 -0.333333V2.33333ZM2.33333 9C2.33333 5.3181 5.3181 2.33333 9 2.33333V-0.333333C3.84534 -0.333333 -0.333333 3.84534 -0.333333 9H2.33333ZM9 15.6667C5.3181 15.6667 2.33333 12.6819 2.33333 9H-0.333333C-0.333333 14.1547 3.84534 18.3333 9 18.3333V15.6667ZM7.5331 5.65836C7.21268 5.62486 6.98008 5.33796 7.01358 5.01754L9.66579 5.29479C9.78542 4.15043 8.95471 3.12577 7.81035 3.00614L7.5331 5.65836ZM8.17392 5.13884C8.14043 5.45925 7.85352 5.69185 7.5331 5.65836L7.81035 3.00614C6.666 2.88652 5.64134 3.71722 5.52171 4.86158L8.17392 5.13884ZM8.01387 6.66994L8.17392 5.13883L5.52171 4.86158L5.36165 6.39268L8.01387 6.66994ZM5.65625 7.86464H6.68776V5.19798H5.65625V7.86464ZM6.23958 7.28131C6.23958 7.60348 5.97842 7.86464 5.65625 7.86464V5.19798C4.50566 5.19798 3.57292 6.13072 3.57292 7.28131H6.23958ZM5.65625 6.69798C5.97842 6.69798 6.23958 6.95914 6.23958 7.28131H3.57292C3.57292 8.4319 4.50566 9.36464 5.65625 9.36464V6.69798ZM6.53095 6.69798H5.65625V9.36464H6.53095V6.69798ZM7.65452 10.1074L7.85706 8.16994L5.20485 7.89268L5.00231 9.83018L7.65452 10.1074ZM5.39062 11.3021H6.32841V8.63548H5.39062V11.3021ZM5.97396 10.7188C5.97396 11.041 5.71279 11.3021 5.39062 11.3021V8.63548C4.24003 8.63548 3.30729 9.56822 3.30729 10.7188H5.97396ZM5.39062 10.1355C5.71279 10.1355 5.97396 10.3966 5.97396 10.7188H3.30729C3.30729 11.8694 4.24003 12.8021 5.39062 12.8021V10.1355ZM6.17161 10.1355H5.39062V12.8021H6.17161V10.1355ZM7.3458 13.0607L7.49772 11.6074L4.8455 11.3302L4.69358 12.7835L7.3458 13.0607ZM6.82627 12.4199C7.14669 12.4534 7.37929 12.7403 7.3458 13.0607L4.69358 12.7835C4.57396 13.9278 5.40466 14.9525 6.54902 15.0721L6.82627 12.4199ZM6.18545 12.9394C6.21895 12.619 6.50585 12.3864 6.82628 12.4199L6.54902 15.0721C7.69338 15.1917 8.71804 14.361 8.83767 13.2167L6.18545 12.9394ZM6.35368 11.3302L6.18545 12.9394L8.83767 13.2167L9.00589 11.6074L6.35368 11.3302ZM9.49973 10.1355H7.67978V12.8021H9.49973V10.1355ZM10.6739 13.0607L10.8258 11.6074L8.17363 11.3302L8.02171 12.7835L10.6739 13.0607ZM10.1544 12.4199C10.4748 12.4534 10.7074 12.7403 10.6739 13.0607L8.02171 12.7835C7.90208 13.9278 8.73279 14.9525 9.87715 15.0721L10.1544 12.4199ZM9.51358 12.9394C9.54707 12.619 9.83398 12.3864 10.1544 12.4199L9.87714 15.0721C11.0215 15.1917 12.0462 14.361 12.1658 13.2167L9.51358 12.9394ZM9.6818 11.3302L9.51358 12.9394L12.1658 13.2167L12.334 11.6074L9.6818 11.3302ZM12.2969 10.1355H11.0079V12.8021H12.2969V10.1355ZM11.7135 10.7188C11.7135 10.3966 11.9747 10.1355 12.2969 10.1355V12.8021C13.4475 12.8021 14.3802 11.8694 14.3802 10.7188H11.7135ZM12.2969 11.3021C11.9747 11.3021 11.7135 11.041 11.7135 10.7188H14.3802C14.3802 9.56822 13.4475 8.63548 12.2969 8.63548V11.3021ZM11.1647 11.3021H12.2969V8.63548H11.1647V11.3021ZM10.0411 7.89268L9.83861 9.83018L12.4908 10.1074L12.6934 8.16994L10.0411 7.89268ZM12.2969 6.69798H11.3673V9.36464H12.2969V6.69798ZM11.7135 7.28131C11.7135 6.95914 11.9747 6.69798 12.2969 6.69798V9.36464C13.4475 9.36464 14.3802 8.4319 14.3802 7.28131H11.7135ZM12.2969 7.86464C11.9747 7.86464 11.7135 7.60348 11.7135 7.28131H14.3802C14.3802 6.13072 13.4475 5.19798 12.2969 5.19798V7.86464ZM11.5241 7.86464H12.2969V5.19798H11.5241V7.86464ZM10.3417 5.01754L10.198 6.39268L12.8502 6.66994L12.9939 5.29479L10.3417 5.01754ZM10.8612 5.65836C10.5408 5.62486 10.3082 5.33796 10.3417 5.01754L12.9939 5.29479C13.1135 4.15043 12.2828 3.12577 11.1385 3.00614L10.8612 5.65836ZM11.502 5.13884C11.4686 5.45925 11.1816 5.69185 10.8612 5.65836L11.1385 3.00614C9.99412 2.88652 8.96946 3.71722 8.84983 4.86158L11.502 5.13884ZM11.342 6.66994L11.502 5.13883L8.84983 4.86158L8.68978 6.39268L11.342 6.66994ZM8.19593 7.86464H10.0159V5.19798H8.19593V7.86464ZM7.01358 5.01754L6.86982 6.39268L9.52204 6.66994L9.66579 5.29479L7.01358 5.01754ZM7.83659 11.3021H9.65654V8.63548H7.83659V11.3021ZM6.71302 7.89268L6.51048 9.83018L9.16269 10.1074L9.36523 8.16994L6.71302 7.89268ZM9.85908 6.69798H8.03913V9.36464H9.85908V6.69798ZM10.9826 10.1074L11.1852 8.16994L8.53297 7.89268L8.33043 9.83018L10.9826 10.1074Z" fill="black" mask="url(#path-1-inside-1)"/>
+</svg>
diff --git a/res/img/element-icons/roomlist/plus-circle.svg b/res/img/element-icons/roomlist/plus-circle.svg
new file mode 100644
index 0000000000..251ded225c
--- /dev/null
+++ b/res/img/element-icons/roomlist/plus-circle.svg
@@ -0,0 +1,3 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M17 9C17 13.4183 13.4183 17 9 17C4.58172 17 1 13.4183 1 9C1 4.58172 4.58172 1 9 1C13.4183 1 17 4.58172 17 9ZM5.25 9C5.25 8.58579 5.58579 8.25 6 8.25H8.25V6C8.25 5.58579 8.58579 5.25 9 5.25C9.41421 5.25 9.75 5.58579 9.75 6V8.25H12C12.4142 8.25 12.75 8.58579 12.75 9C12.75 9.41421 12.4142 9.75 12 9.75H9.75V12C9.75 12.4142 9.41421 12.75 9 12.75C8.58579 12.75 8.25 12.4142 8.25 12V9.75H6C5.58579 9.75 5.25 9.41421 5.25 9Z" fill="black"/>
+</svg>
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 45db15df7c..f7da6571da 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -47,6 +47,9 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
 import AccessibleButton from "../elements/AccessibleButton";
 import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
 import CallHandler from "../../../CallHandler";
+import SpaceStore from "../../../stores/SpaceStore";
+import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
+import { EventType } from "matrix-js-sdk/src/@types/event";
 
 interface IProps {
     onKeyDown: (ev: React.KeyboardEvent) => void;
@@ -152,6 +155,50 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
         defaultHidden: false,
         addRoomLabel: _td("Add room"),
         addRoomContextMenu: (onFinished: () => void) => {
+            if (SpaceStore.instance.activeSpace) {
+                const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
+                    MatrixClientPeg.get().getUserId());
+
+                return <IconizedContextMenuOptionList first>
+                    <IconizedContextMenuOption
+                        label={_t("Create new room")}
+                        iconClassName="mx_RoomList_iconPlus"
+                        onClick={(e) => {
+                            e.preventDefault();
+                            e.stopPropagation();
+                            onFinished();
+                            showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
+                        }}
+                        disabled={!canAddRooms}
+                        tooltip={canAddRooms ? undefined
+                            : _t("You do not have permissions to create new rooms in this space")}
+                    />
+                    <IconizedContextMenuOption
+                        label={_t("Add existing room")}
+                        iconClassName="mx_RoomList_iconHash"
+                        onClick={(e) => {
+                            e.preventDefault();
+                            e.stopPropagation();
+                            onFinished();
+                            showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
+                        }}
+                        disabled={!canAddRooms}
+                        tooltip={canAddRooms ? undefined
+                            : _t("You do not have permissions to add rooms to this space")}
+                    />
+                    <IconizedContextMenuOption
+                        label={_t("Explore space rooms")}
+                        iconClassName="mx_RoomList_iconExplore"
+                        onClick={(e) => {
+                            e.preventDefault();
+                            e.stopPropagation();
+                            onFinished();
+                            defaultDispatcher.fire(Action.ViewRoomDirectory);
+                        }}
+                    />
+                </IconizedContextMenuOptionList>;
+            }
+
             return <IconizedContextMenuOptionList first>
                 <IconizedContextMenuOption
                     label={_t("Create new room")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 04a6d63272..6d2f41ceae 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1509,6 +1509,10 @@
     "Rooms": "Rooms",
     "Add room": "Add room",
     "Create new room": "Create new room",
+    "You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
+    "Add existing room": "Add existing room",
+    "You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
+    "Explore space rooms": "Explore space rooms",
     "Explore community rooms": "Explore community rooms",
     "Explore public rooms": "Explore public rooms",
     "Low priority": "Low priority",

From 716268b2f953e54345389d08c2a95825e7590a7a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 14:34:47 +0000
Subject: [PATCH 255/389] Add context menu to spaces in the space panel

---
 res/css/structures/_SpacePanel.scss           |  75 ++++++
 src/accessibility/context_menu/MenuItem.tsx   |  11 +-
 src/components/views/dialogs/InfoDialog.js    |   6 +-
 .../views/spaces/SpaceTreeLevel.tsx           | 216 ++++++++++++++++++
 src/i18n/strings/en_EN.json                   |  16 +-
 5 files changed, 314 insertions(+), 10 deletions(-)

diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss
index 24d2243912..9937117086 100644
--- a/res/css/structures/_SpacePanel.scss
+++ b/res/css/structures/_SpacePanel.scss
@@ -212,6 +212,30 @@ $activeBorderColor: $secondary-fg-color;
                 border-radius: 8px;
             }
         }
+
+        .mx_SpaceButton_menuButton {
+            width: 20px;
+            min-width: 20px; // yay flex
+            height: 20px;
+            margin-top: auto;
+            margin-bottom: auto;
+            position: relative;
+            display: none;
+
+            &::before {
+                top: 2px;
+                left: 2px;
+                content: '';
+                width: 16px;
+                height: 16px;
+                position: absolute;
+                mask-position: center;
+                mask-size: contain;
+                mask-repeat: no-repeat;
+                mask-image: url('$(res)/img/element-icons/context-menu.svg');
+                background: $primary-fg-color;
+            }
+        }
     }
 
     .mx_SpacePanel_badgeContainer {
@@ -254,6 +278,10 @@ $activeBorderColor: $secondary-fg-color;
                 height: 0;
                 display: none;
             }
+
+            .mx_SpaceButton_menuButton {
+                display: block;
+            }
         }
     }
 
@@ -272,3 +300,50 @@ $activeBorderColor: $secondary-fg-color;
         }
     }
 }
+
+.mx_SpacePanel_contextMenu {
+    .mx_SpacePanel_contextMenu_header {
+        margin: 12px 16px 12px;
+        font-weight: $font-semi-bold;
+        font-size: $font-15px;
+        line-height: $font-18px;
+    }
+
+    .mx_IconizedContextMenu_optionList .mx_AccessibleButton.mx_SpacePanel_contextMenu_inviteButton {
+        color: $accent-color;
+
+        .mx_SpacePanel_iconInvite::before {
+            background-color: $accent-color;
+            mask-image: url('$(res)/img/element-icons/room/invite.svg');
+        }
+    }
+
+    .mx_SpacePanel_iconSettings::before {
+        mask-image: url('$(res)/img/element-icons/settings.svg');
+    }
+
+    .mx_SpacePanel_iconLeave::before {
+        mask-image: url('$(res)/img/element-icons/leave.svg');
+    }
+
+    .mx_SpacePanel_iconHome::before {
+        mask-image: url('$(res)/img/element-icons/roomlist/home.svg');
+    }
+
+    .mx_SpacePanel_iconMembers::before {
+        mask-image: url('$(res)/img/element-icons/room/members.svg');
+    }
+
+    .mx_SpacePanel_iconPlus::before {
+        mask-image: url('$(res)/img/element-icons/plus.svg');
+    }
+
+    .mx_SpacePanel_iconExplore::before {
+        mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
+    }
+}
+
+
+.mx_SpacePanel_sharePublicSpace {
+    margin: 0;
+}
diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx
index 0bb169abf8..9a7c1d1f0a 100644
--- a/src/accessibility/context_menu/MenuItem.tsx
+++ b/src/accessibility/context_menu/MenuItem.tsx
@@ -19,14 +19,23 @@ limitations under the License.
 import React from "react";
 
 import AccessibleButton from "../../components/views/elements/AccessibleButton";
+import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton";
 
 interface IProps extends React.ComponentProps<typeof AccessibleButton> {
     label?: string;
+    tooltip?: string;
 }
 
 // Semantic component for representing a role=menuitem
-export const MenuItem: React.FC<IProps> = ({children, label, ...props}) => {
+export const MenuItem: React.FC<IProps> = ({children, label, tooltip, ...props}) => {
     const ariaLabel = props["aria-label"] || label;
+
+    if (tooltip) {
+        return <AccessibleTooltipButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel} title={tooltip}>
+            { children }
+        </AccessibleTooltipButton>;
+    }
+
     return (
         <AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel}>
             { children }
diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js
index 97ae968ff3..6dc9fc01b0 100644
--- a/src/components/views/dialogs/InfoDialog.js
+++ b/src/components/views/dialogs/InfoDialog.js
@@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component {
         className: PropTypes.string,
         title: PropTypes.string,
         description: PropTypes.node,
-        button: PropTypes.string,
+        button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool),
         onFinished: PropTypes.func,
         hasCloseButton: PropTypes.bool,
         onKeyDown: PropTypes.func,
@@ -60,11 +60,11 @@ export default class InfoDialog extends React.Component {
                 <div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
                     { this.props.description }
                 </div>
-                <DialogButtons primaryButton={this.props.button || _t('OK')}
+                { this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')}
                     onPrimaryButtonClick={this.onFinished}
                     hasCancel={false}
                 >
-                </DialogButtons>
+                </DialogButtons> }
             </BaseDialog>
         );
     }
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index f94798433f..04d6c02208 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -23,7 +23,27 @@ import SpaceStore from "../../../stores/SpaceStore";
 import NotificationBadge from "../rooms/NotificationBadge";
 import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
 import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
+import IconizedContextMenu, {
+    IconizedContextMenuOption,
+    IconizedContextMenuOptionList,
+} from "../context_menus/IconizedContextMenu";
+import {_t} from "../../../languageHandler";
+import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
+import {toRightOf} from "../../structures/ContextMenu";
+import {shouldShowSpaceSettings, showCreateNewRoom, showSpaceSettings} from "../../../utils/space";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {ButtonEvent} from "../elements/AccessibleButton";
+import defaultDispatcher from "../../../dispatcher/dispatcher";
+import Modal from "../../../Modal";
+import SpacePublicShare from "./SpacePublicShare";
+import {Action} from "../../../dispatcher/actions";
+import RoomViewStore from "../../../stores/RoomViewStore";
+import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
+import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
+import {showRoomInviteDialog} from "../../../RoomInvite";
+import InfoDialog from "../dialogs/InfoDialog";
+import {EventType} from "matrix-js-sdk/src/@types/event";
+import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory";
 
 interface IItemProps {
     space?: Room;
@@ -78,6 +98,200 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
         SpaceStore.instance.setActiveSpace(this.props.space);
     };
 
+    private onMenuOpenClick = (ev: React.MouseEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+        const target = ev.target as HTMLButtonElement;
+        this.setState({contextMenuPosition: target.getBoundingClientRect()});
+    };
+
+    private onMenuClose = () => {
+        this.setState({contextMenuPosition: null});
+    };
+
+    private onHomeClick = (ev: ButtonEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+
+        defaultDispatcher.dispatch({
+            action: "view_room",
+            room_id: this.props.space.roomId,
+        });
+        this.setState({contextMenuPosition: null}); // also close the menu
+    };
+
+    private onInviteClick = (ev: ButtonEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+
+        if (this.props.space.getJoinRule() === "public") {
+            const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
+                title: _t("Invite members"),
+                description: <React.Fragment>
+                    <span>{ _t("Share your public space") }</span>
+                    <SpacePublicShare space={this.props.space} onFinished={() => modal.close()} />
+                </React.Fragment>,
+                fixedWidth: false,
+                button: false,
+                className: "mx_SpacePanel_sharePublicSpace",
+                hasCloseButton: true,
+            });
+        } else {
+            showRoomInviteDialog(this.props.space.roomId);
+        }
+        this.setState({contextMenuPosition: null}); // also close the menu
+    };
+
+    private onSettingsClick = (ev: ButtonEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+
+        showSpaceSettings(this.context, this.props.space);
+        this.setState({contextMenuPosition: null}); // also close the menu
+    };
+
+    private onLeaveClick = (ev: ButtonEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+
+        defaultDispatcher.dispatch({
+            action: "leave_room",
+            room_id: this.props.space.roomId,
+        });
+        this.setState({contextMenuPosition: null}); // also close the menu
+    };
+
+    private onNewRoomClick = (ev: ButtonEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+
+        showCreateNewRoom(this.context, this.props.space);
+        this.setState({contextMenuPosition: null}); // also close the menu
+    };
+
+    private onMembersClick = (ev: ButtonEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+
+        if (!RoomViewStore.getRoomId()) {
+            defaultDispatcher.dispatch({
+                action: "view_room",
+                room_id: this.props.space.roomId,
+            }, true);
+        }
+
+        defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
+            action: Action.SetRightPanelPhase,
+            phase: RightPanelPhases.SpaceMemberList,
+            refireParams: { space: this.props.space },
+        });
+        this.setState({contextMenuPosition: null}); // also close the menu
+    };
+
+    private onExploreRoomsClick = (ev: ButtonEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+
+        Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, {
+            space: this.props.space,
+        }, "mx_SpaceRoomDirectory_dialogWrapper", false, true);
+        this.setState({contextMenuPosition: null}); // also close the menu
+    };
+
+    private renderContextMenu(): React.ReactElement {
+        let contextMenu = null;
+        if (this.state.contextMenuPosition) {
+            const userId = this.context.getUserId();
+
+            let inviteOption;
+            if (this.props.space.canInvite(userId)) {
+                inviteOption = (
+                    <IconizedContextMenuOption
+                        className="mx_SpacePanel_contextMenu_inviteButton"
+                        iconClassName="mx_SpacePanel_iconInvite"
+                        label={_t("Invite people")}
+                        onClick={this.onInviteClick}
+                    />
+                );
+            }
+
+            let settingsOption;
+            let leaveSection;
+            if (shouldShowSpaceSettings(this.context, this.props.space)) {
+                settingsOption = (
+                    <IconizedContextMenuOption
+                        iconClassName="mx_SpacePanel_iconSettings"
+                        label={_t("Settings")}
+                        onClick={this.onSettingsClick}
+                    />
+                );
+            } else {
+                leaveSection = <IconizedContextMenuOptionList red first>
+                    <IconizedContextMenuOption
+                        iconClassName="mx_SpacePanel_iconLeave"
+                        label={_t("Leave space")}
+                        onClick={this.onLeaveClick}
+                    />
+                </IconizedContextMenuOptionList>;
+            }
+
+            let newRoomOption;
+            if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
+                newRoomOption = (
+                    <IconizedContextMenuOption
+                        iconClassName="mx_SpacePanel_iconPlus"
+                        label={_t("New room")}
+                        onClick={this.onNewRoomClick}
+                    />
+                );
+            }
+
+            contextMenu = <IconizedContextMenu
+                {...toRightOf(this.state.contextMenuPosition, 0)}
+                onFinished={this.onMenuClose}
+                className="mx_SpacePanel_contextMenu"
+                compact
+            >
+                <div className="mx_SpacePanel_contextMenu_header">
+                    { this.props.space.name }
+                </div>
+                <IconizedContextMenuOptionList first>
+                    { inviteOption }
+                    <IconizedContextMenuOption
+                        iconClassName="mx_SpacePanel_iconHome"
+                        label={_t("Space Home")}
+                        onClick={this.onHomeClick}
+                    />
+                    <IconizedContextMenuOption
+                        iconClassName="mx_SpacePanel_iconMembers"
+                        label={_t("Members")}
+                        onClick={this.onMembersClick}
+                    />
+                    { settingsOption }
+                    <IconizedContextMenuOption
+                        iconClassName="mx_SpacePanel_iconExplore"
+                        label={_t("Explore rooms")}
+                        onClick={this.onExploreRoomsClick}
+                    />
+                    { newRoomOption }
+                </IconizedContextMenuOptionList>
+                { leaveSection }
+            </IconizedContextMenu>;
+        }
+
+        return (
+            <React.Fragment>
+                <ContextMenuTooltipButton
+                    className="mx_SpaceButton_menuButton"
+                    onClick={this.onMenuOpenClick}
+                    title={_t("Space options")}
+                    isExpanded={!!this.state.contextMenuPosition}
+                />
+                { contextMenu }
+            </React.Fragment>
+        );
+    }
+
     render() {
         const {space, activeSpaces, isNested} = this.props;
 
@@ -133,6 +347,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
                     <div className="mx_SpaceButton_selectionWrapper">
                         <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
                         { notifBadge }
+                        { this.renderContextMenu() }
                     </div>
                 </RovingAccessibleTooltipButton>
             );
@@ -149,6 +364,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
                         <RoomAvatar width={avatarSize} height={avatarSize} room={space} />
                         <span className="mx_SpaceButton_name">{ space.name }</span>
                         { notifBadge }
+                        { this.renderContextMenu() }
                     </div>
                 </RovingAccessibleButton>
             );
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 6d2f41ceae..19324e1540 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1003,6 +1003,16 @@
     "Failed to copy": "Failed to copy",
     "Share invite link": "Share invite link",
     "Invite by email or username": "Invite by email or username",
+    "Invite members": "Invite members",
+    "Share your public space": "Share your public space",
+    "Invite people": "Invite people",
+    "Settings": "Settings",
+    "Leave space": "Leave space",
+    "New room": "New room",
+    "Space Home": "Space Home",
+    "Members": "Members",
+    "Explore rooms": "Explore rooms",
+    "Space options": "Space options",
     "Remove": "Remove",
     "This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
     "This bridge is managed by <user />.": "This bridge is managed by <user />.",
@@ -1583,7 +1593,6 @@
     "Favourited": "Favourited",
     "Favourite": "Favourite",
     "Low Priority": "Low Priority",
-    "Settings": "Settings",
     "Leave Room": "Leave Room",
     "Room options": "Room options",
     "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
@@ -1672,7 +1681,6 @@
     "The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to",
     "Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection",
     "Yours, or the other users’ session": "Yours, or the other users’ session",
-    "Members": "Members",
     "Room Info": "Room Info",
     "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
     "Unpin": "Unpin",
@@ -2510,13 +2518,11 @@
     "Explore Public Rooms": "Explore Public Rooms",
     "Create a Group Chat": "Create a Group Chat",
     "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s",
-    "Explore rooms": "Explore rooms",
     "Failed to reject invitation": "Failed to reject invitation",
     "Cannot create rooms in this community": "Cannot create rooms in this community",
     "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",
     "This space is not public. You will not be able to rejoin without an invite.": "This space is not public. You will not be able to rejoin without an invite.",
     "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
-    "Leave space": "Leave space",
     "Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?",
     "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
     "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
@@ -2592,7 +2598,6 @@
     "Manage rooms": "Manage rooms",
     "Find a room...": "Find a room...",
     "Accept Invite": "Accept Invite",
-    "Invite people": "Invite people",
     "Add existing rooms & spaces": "Add existing rooms & spaces",
     "%(count)s members|other": "%(count)s members",
     "%(count)s members|one": "%(count)s member",
@@ -2607,7 +2612,6 @@
     "Failed to create initial space rooms": "Failed to create initial space rooms",
     "Skip for now": "Skip for now",
     "Creating rooms...": "Creating rooms...",
-    "Share your public space": "Share your public space",
     "At the moment only you can see it.": "At the moment only you can see it.",
     "Finish": "Finish",
     "Who are you working with?": "Who are you working with?",

From 43cc7deedae82893a96f22493a0e360399f12968 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 14:37:28 +0000
Subject: [PATCH 256/389] Show hierarchy of auto_join rooms in the space view

---
 res/css/structures/_SpaceRoomView.scss      |  8 +++
 src/components/structures/SpaceRoomView.tsx | 59 ++++++++++++++++++++-
 src/i18n/strings/en_EN.json                 |  2 +
 3 files changed, 67 insertions(+), 2 deletions(-)

diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index ee60389c59..38310d39a9 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -219,6 +219,14 @@ $SpaceRoomViewInnerWidth: 428px;
                 }
             }
         }
+
+        .mx_SpaceRoomDirectory_list {
+            max-width: 600px;
+
+            .mx_SpaceRoomDirectory_roomTile_actions {
+                display: none;
+            }
+        }
     }
 
     .mx_SpaceRoomView_privateScope {
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index f1a8a4d71b..5c91efc1c0 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import React, {RefObject, useContext, useRef, useState} from "react";
-import {EventType} from "matrix-js-sdk/src/@types/event";
+import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
 import {Room} from "matrix-js-sdk/src/models/room";
 
 import MatrixClientContext from "../../contexts/MatrixClientContext";
@@ -24,6 +24,7 @@ import {_t} from "../../languageHandler";
 import AccessibleButton from "../views/elements/AccessibleButton";
 import RoomName from "../views/elements/RoomName";
 import RoomTopic from "../views/elements/RoomTopic";
+import InlineSpinner from "../views/elements/InlineSpinner";
 import FormButton from "../views/elements/FormButton";
 import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
 import {useRoomMembers} from "../../hooks/useRoomMembers";
@@ -47,7 +48,12 @@ import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanel
 import {useStateArray} from "../../hooks/useStateArray";
 import SpacePublicShare from "../views/spaces/SpacePublicShare";
 import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
+import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom} from "./SpaceRoomDirectory";
+import {useAsyncMemo} from "../../hooks/useAsyncMemo";
+import {EnhancedMap} from "../../utils/maps";
+import AutoHideScrollbar from "./AutoHideScrollbar";
 import MemberAvatar from "../views/avatars/MemberAvatar";
+import {useStateToggle} from "../../hooks/useStateToggle";
 
 interface IProps {
     space: Room;
@@ -121,13 +127,15 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
 
     const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
 
+    const [_, forceUpdate] = useStateToggle(false); // TODO
+
     let addRoomButtons;
     if (canAddRooms) {
         addRoomButtons = <React.Fragment>
             <AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
                 const [added] = await showAddExistingRooms(cli, space);
                 if (added) {
-                    // TODO update rooms shown once we show hierarchy here
+                    forceUpdate();
                 }
             }}>
                 { _t("Add existing rooms & spaces") }
@@ -149,6 +157,51 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
         </AccessibleButton>;
     }
 
+    const [loading, roomsMap, relations, numRooms] = useAsyncMemo(async () => {
+        try {
+            const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join");
+
+            const parentChildRelations = new EnhancedMap<string, string[]>();
+            data.events.map((ev: ISpaceSummaryEvent) => {
+                if (ev.type === EventType.SpaceChild) {
+                    parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
+                }
+            });
+
+            const roomsMap = new Map<string, ISpaceSummaryRoom>(data.rooms.map(r => [r.room_id, r]));
+            const numRooms = data.rooms.filter(r => r.room_type !== RoomType.Space).length;
+            return [false, roomsMap, parentChildRelations, numRooms];
+        } catch (e) {
+            console.error(e); // TODO
+        }
+
+        return [false];
+    }, [space, _], [true]);
+
+    let previewRooms;
+    if (roomsMap) {
+        previewRooms = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
+            <div className="mx_SpaceRoomDirectory_roomCount">
+                <h3>{ myMembership === "join" ? _t("Rooms") : _t("Default Rooms")}</h3>
+                <span>{ numRooms }</span>
+            </div>
+            <HierarchyLevel
+                spaceId={space.roomId}
+                rooms={roomsMap}
+                editing={false}
+                relations={relations}
+                parents={new Set()}
+                onPreviewClick={roomId => {
+                    showRoom(roomsMap.get(roomId), [], false); // TODO
+                }}
+            />
+        </AutoHideScrollbar>;
+    } else if (loading) {
+        previewRooms = <InlineSpinner />;
+    } else {
+        previewRooms = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
+    }
+
     return <div className="mx_SpaceRoomView_landing">
         <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
         <div className="mx_SpaceRoomView_landing_name">
@@ -213,6 +266,8 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
             { addRoomButtons }
             { settingsButton }
         </div>
+
+        { previewRooms }
     </div>;
 };
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 19324e1540..8609af6a71 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2599,6 +2599,8 @@
     "Find a room...": "Find a room...",
     "Accept Invite": "Accept Invite",
     "Add existing rooms & spaces": "Add existing rooms & spaces",
+    "Default Rooms": "Default Rooms",
+    "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
     "%(count)s members|other": "%(count)s members",
     "%(count)s members|one": "%(count)s member",
     "<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",

From 20e57d15fd2d77e9dc783ef55480a963a5329054 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 2 Mar 2021 15:20:54 +0000
Subject: [PATCH 257/389] Option for audio streaming

---
 .../views/context_menus/WidgetContextMenu.tsx       | 13 +++++++++++++
 src/i18n/strings/en_EN.json                         |  1 +
 src/stores/widgets/ElementWidgetActions.ts          |  1 +
 3 files changed, 15 insertions(+)

diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx
index c1af86eae6..e7d1c02c66 100644
--- a/src/components/views/context_menus/WidgetContextMenu.tsx
+++ b/src/components/views/context_menus/WidgetContextMenu.tsx
@@ -31,6 +31,7 @@ import QuestionDialog from "../dialogs/QuestionDialog";
 import {WidgetType} from "../../../widgets/WidgetType";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
+import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
 
 interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
     app: IApp;
@@ -54,6 +55,17 @@ const WidgetContextMenu: React.FC<IProps> = ({
     const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
     const canModify = userWidget || WidgetUtils.canUserModifyWidgets(roomId);
 
+    let streamAudioStreamButton;
+    if (getConfigLivestreamUrl() && (app.type === "m.jitsi" || app.type === "jitsi")) {
+        const onStreamAudioClick = () => {
+            startJitsiAudioLivestream(widgetMessaging, roomId);
+            onFinished();
+        };
+        streamAudioStreamButton = <IconizedContextMenuOption
+            onClick={onStreamAudioClick} label={_t("Start audio stream")}
+        />;
+    }
+
     let unpinButton;
     if (showUnpin) {
         const onUnpinClick = () => {
@@ -163,6 +175,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
 
     return <IconizedContextMenu {...props} chevronFace={ChevronFace.None} onFinished={onFinished}>
         <IconizedContextMenuOptionList>
+            { streamAudioStreamButton }
             { editButton }
             { revokeButton }
             { deleteButton }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 38460a5f6e..7242ed7de6 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2357,6 +2357,7 @@
     "Set status": "Set status",
     "Set a new status...": "Set a new status...",
     "View Community": "View Community",
+    "Start audio stream": "Start audio stream",
     "Take a picture": "Take a picture",
     "Delete Widget": "Delete Widget",
     "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
diff --git a/src/stores/widgets/ElementWidgetActions.ts b/src/stores/widgets/ElementWidgetActions.ts
index de48746a74..cd591a6fb4 100644
--- a/src/stores/widgets/ElementWidgetActions.ts
+++ b/src/stores/widgets/ElementWidgetActions.ts
@@ -19,6 +19,7 @@ import { IWidgetApiRequest } from "matrix-widget-api";
 export enum ElementWidgetActions {
     ClientReady = "im.vector.ready",
     HangupCall = "im.vector.hangup",
+    StartLiveStream = "im.vector.start_live_stream",
     OpenIntegrationManager = "integration_manager_open",
 
     /**

From d1a75885a7a65477ab106e65b745a6e2e972daf6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 2 Mar 2021 15:35:02 +0000
Subject: [PATCH 258/389] Protect onAction dispatch handler on the SpaceStore
 with Spaces disabled

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

diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index d675879138..8e0066da91 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -408,6 +408,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
     }
 
     protected async onAction(payload: ActionPayload) {
+        if (!SettingsStore.getValue("feature_spaces")) return;
         switch (payload.action) {
             case "view_room": {
                 const room = this.matrixClient?.getRoom(payload.room_id);

From 867ce322e1da4675b78bb884256bbcef423f585e Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 2 Mar 2021 10:47:49 -0700
Subject: [PATCH 259/389] Add .tmp files to gitignore

My git client is convinced that `src/component-index.js.tmp` needs to be checked in, which is nice of it but also wrong.
---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitignore b/.gitignore
index 33e8bfc7ac..e1dd7726e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ package-lock.json
 /src/component-index.js
 
 .DS_Store
+*.tmp

From 08d35073de93367b059b23c1581b8eb765822754 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 2 Mar 2021 11:04:12 -0700
Subject: [PATCH 260/389] Improve commentary

---
 src/components/views/messages/EditHistoryMessage.js | 2 +-
 src/components/views/rooms/EventTile.js             | 5 +++--
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
index 6c420a16fc..0967be937a 100644
--- a/src/components/views/messages/EditHistoryMessage.js
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -158,7 +158,7 @@ export default class EditHistoryMessage extends React.PureComponent {
         const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.state.sendStatus) !== -1);
         const classes = classNames({
             "mx_EventTile": true,
-            // Note: we keep these sending state classes for tests, not for our styles
+            // Note: we keep the `sending` state class for tests, not for our styles
             "mx_EventTile_sending": isSending,
             "mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
         });
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 01e932dd3a..b4192fc8d3 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -408,6 +408,8 @@ export default class EventTile extends React.Component {
             return;
         }
 
+        // We force update because we have no state or prop changes to queue up, instead relying on
+        // the getters we use here to determine what needs rendering.
         this.forceUpdate(() => {
             // Per elsewhere in this file, we can remove the listener once we will have no further purpose for it.
             if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt) {
@@ -805,8 +807,7 @@ export default class EventTile extends React.Component {
             mx_EventTile_isEditing: isEditing,
             mx_EventTile_info: isInfoMessage,
             mx_EventTile_12hr: this.props.isTwelveHour,
-            // Note: we keep these sending state classes for tests, not for our styles
-            mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
+            // Note: we keep the `sending` state class for tests, not for our styles
             mx_EventTile_sending: !isEditing && isSending,
             mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent',
             mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),

From d9a801910a44a80c63035fcaab5769450951c612 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 3 Mar 2021 11:34:29 +0000
Subject: [PATCH 261/389] Tweak spaces copy

---
 src/RoomInvite.js                             |  7 ++---
 src/components/structures/SpaceRoomView.tsx   |  2 +-
 src/components/views/dialogs/InviteDialog.tsx | 31 +++++++++++--------
 .../views/spaces/SpaceCreateMenu.tsx          | 11 +++----
 src/i18n/strings/en_EN.json                   | 18 ++++++-----
 5 files changed, 37 insertions(+), 32 deletions(-)

diff --git a/src/RoomInvite.js b/src/RoomInvite.js
index 503411d2b3..9ae41b851a 100644
--- a/src/RoomInvite.js
+++ b/src/RoomInvite.js
@@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter';
 import Modal from './Modal';
 import * as sdk from './';
 import { _t } from './languageHandler';
-import InviteDialog, {KIND_DM, KIND_INVITE, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog";
+import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
 import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
 import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
 
@@ -50,11 +50,10 @@ export function showStartChatInviteDialog(initialText) {
 }
 
 export function showRoomInviteDialog(roomId) {
-    const isSpace = MatrixClientPeg.get()?.getRoom(roomId)?.isSpaceRoom();
     // This dialog handles the room creation internally - we don't need to worry about it.
     Modal.createTrackedDialog(
-        "Invite Users", isSpace ? "Space" : "Room", InviteDialog, {
-            kind: isSpace ? KIND_SPACE_INVITE : KIND_INVITE,
+        "Invite Users", "", InviteDialog, {
+            kind: KIND_INVITE,
             roomId,
         },
         /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 5c91efc1c0..9bacdd975d 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -557,7 +557,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
             case Phase.PublicCreateRooms:
                 return <SpaceSetupFirstRooms
                     space={this.props.space}
-                    title={_t("What discussions do you want to have?")}
+                    title={_t("What are some things you want to discuss?")}
                     description={_t("We'll create rooms for each topic.")}
                     onFinished={() => this.setState({ phase: Phase.PublicShare })}
                 />;
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 9bc5b6476f..6d5cb8786e 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -48,7 +48,6 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 
 export const KIND_DM = "dm";
 export const KIND_INVITE = "invite";
-export const KIND_SPACE_INVITE = "space_invite";
 export const KIND_CALL_TRANSFER = "call_transfer";
 
 const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
@@ -310,7 +309,7 @@ interface IInviteDialogProps {
     // not provided.
     kind: string,
 
-    // The room ID this dialog is for. Only required for KIND_INVITE and KIND_SPACE_INVITE.
+    // The room ID this dialog is for. Only required for KIND_INVITE.
     roomId: string,
 
     // The call to transfer. Only required for KIND_CALL_TRANSFER.
@@ -349,8 +348,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
     constructor(props) {
         super(props);
 
-        if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) {
-            throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog");
+        if ((props.kind === KIND_INVITE) && !props.roomId) {
+            throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
         } else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
             throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
         }
@@ -1027,7 +1026,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             sectionSubname = _t("May include members not in %(communityName)s", {communityName});
         }
 
-        if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
+        if (this.props.kind === KIND_INVITE) {
             sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
         }
 
@@ -1248,25 +1247,31 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             }
             buttonText = _t("Go");
             goButtonFn = this._startDm;
-        } else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
-            title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space");
+        } else if (this.props.kind === KIND_INVITE) {
+            const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
+            const isSpace = room?.isSpaceRoom();
+            title = isSpace
+                ? _t("Invite to %(spaceName)s", {
+                    spaceName: room.name || _t("Unnamed Space"),
+                })
+                : _t("Invite to this room");
 
             let helpTextUntranslated;
-            if (this.props.kind === KIND_INVITE) {
+            if (isSpace) {
                 if (identityServersEnabled) {
                     helpTextUntranslated = _td("Invite someone using their name, email address, username " +
-                        "(like <userId/>) or <a>share this room</a>.");
+                        "(like <userId/>) or <a>share this space</a>.");
                 } else {
                     helpTextUntranslated = _td("Invite someone using their name, username " +
-                        "(like <userId/>) or <a>share this room</a>.");
+                        "(like <userId/>) or <a>share this space</a>.");
                 }
-            } else { // KIND_SPACE_INVITE
+            } else {
                 if (identityServersEnabled) {
                     helpTextUntranslated = _td("Invite someone using their name, email address, username " +
-                        "(like <userId/>) or <a>share this space</a>.");
+                        "(like <userId/>) or <a>share this room</a>.");
                 } else {
                     helpTextUntranslated = _td("Invite someone using their name, username " +
-                        "(like <userId/>) or <a>share this space</a>.");
+                        "(like <userId/>) or <a>share this room</a>.");
                 }
             }
 
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 9d0543a6c5..88098d1b66 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -107,7 +107,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
     if (visibility === null) {
         body = <React.Fragment>
             <h2>{ _t("Create a space") }</h2>
-            <p>{ _t("Organise rooms into spaces, for just you or anyone") }</p>
+            <p>{ _t("Spaces are new ways to group rooms and people. " +
+                "To join an existing space you’ll need an invite") }</p>
 
             <SpaceCreateMenuType
                 title={_t("Public")}
@@ -117,12 +118,12 @@ const SpaceCreateMenu = ({ onFinished }) => {
             />
             <SpaceCreateMenuType
                 title={_t("Private")}
-                description={_t("Invite only space, best for yourself or teams")}
+                description={_t("Invite only, best for yourself or teams")}
                 className="mx_SpaceCreateMenuType_private"
                 onClick={() => setVisibility(Visibility.Private)}
             />
 
-            {/*<p>{ _t("Looking to join an existing space?") }</p>*/}
+            <p>{ _t("You can change this later") }</p>
         </React.Fragment>;
     } else {
         body = <React.Fragment>
@@ -134,9 +135,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
 
             <h2>
                 {
-                    visibility === Visibility.Public
-                        ? _t("Personalise your public space")
-                        : _t("Personalise your private space")
+                    visibility === Visibility.Public ? _t("Your public space") : _t("Your private space")
                 }
             </h2>
             <p>
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 8609af6a71..e256e6bb2f 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -983,14 +983,15 @@
     "Name": "Name",
     "Description": "Description",
     "Create a space": "Create a space",
-    "Organise rooms into spaces, for just you or anyone": "Organise rooms into spaces, for just you or anyone",
+    "Spaces are new ways to group rooms and people. To join an existing space you’ll need an invite": "Spaces are new ways to group rooms and people. To join an existing space you’ll need an invite",
     "Public": "Public",
     "Open space for anyone, best for communities": "Open space for anyone, best for communities",
     "Private": "Private",
-    "Invite only space, best for yourself or teams": "Invite only space, best for yourself or teams",
+    "Invite only, best for yourself or teams": "Invite only, best for yourself or teams",
+    "You can change this later": "You can change this later",
     "Go back": "Go back",
-    "Personalise your public space": "Personalise your public space",
-    "Personalise your private space": "Personalise your private space",
+    "Your public space": "Your public space",
+    "Your private space": "Your private space",
     "Give it a photo, name and description to help you identify it.": "Give it a photo, name and description to help you identify it.",
     "You can change these at any point.": "You can change these at any point.",
     "Creating...": "Creating...",
@@ -2190,10 +2191,12 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
     "Go": "Go",
-    "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
-    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
+    "Invite to %(spaceName)s": "Invite to %(spaceName)s",
+    "Unnamed Space": "Unnamed Space",
     "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
     "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
+    "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
     "Transfer": "Transfer",
     "a new master key signature": "a new master key signature",
     "a new cross-signing key signature": "a new cross-signing key signature",
@@ -2587,7 +2590,6 @@
     "Failed to reject invite": "Failed to reject invite",
     "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
     "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
-    "Unnamed Space": "Unnamed Space",
     "Undo": "Undo",
     "Remove from Space": "Remove from Space",
     "No permissions": "No permissions",
@@ -2626,7 +2628,7 @@
     "Invite your teammates": "Invite your teammates",
     "Invite by username": "Invite by username",
     "Inviting...": "Inviting...",
-    "What discussions do you want to have?": "What discussions do you want to have?",
+    "What are some things you want to discuss?": "What are some things you want to discuss?",
     "We'll create rooms for each topic.": "We'll create rooms for each topic.",
     "What projects are you working on?": "What projects are you working on?",
     "We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.",

From 85985db441da38bf3872f6483d15758af49f1b15 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 3 Mar 2021 11:50:41 +0000
Subject: [PATCH 262/389] add comment

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

diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index 631ddc484f..075e9ff585 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -46,6 +46,8 @@ limitations under the License.
     }
 
     .mx_RightPanel_scopeHeader {
+        // vertically align with position on other right panel cards
+        // to prevent it bouncing as user navigates right panel
         margin-top: -8px;
     }
 }

From 73411fa53dd9b8a2f3b9300779ee1bfe45c8c0f9 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 3 Mar 2021 13:42:44 +0000
Subject: [PATCH 263/389] tidy code style

---
 src/components/structures/SpaceRoomDirectory.tsx | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index 7f7b9dbb99..06df6a528e 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -157,10 +157,12 @@ const SubSpace: React.FC<ISubspaceProps> = ({
 
     let url: string;
     if (space.avatar_url) {
-        url = MatrixClientPeg.get().mxcUrlToHttp(space.avatar_url,
+        url = MatrixClientPeg.get().mxcUrlToHttp(
+            space.avatar_url,
             Math.floor(24 * window.devicePixelRatio),
             Math.floor(24 * window.devicePixelRatio),
-            "crop");
+            "crop",
+        );
     }
 
     return <div className="mx_SpaceRoomDirectory_subspace">
@@ -262,10 +264,12 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
 
     let url: string;
     if (room.avatar_url) {
-        url = cli.mxcUrlToHttp(room.avatar_url,
+        url = cli.mxcUrlToHttp(
+            room.avatar_url,
             Math.floor(32 * window.devicePixelRatio),
             Math.floor(32 * window.devicePixelRatio),
-            "crop");
+            "crop",
+        );
     }
 
     const content = <React.Fragment>

From d8483ddf0de4a08074a3c59ed0eff71da1390e74 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Wed, 3 Mar 2021 20:23:21 +0000
Subject: [PATCH 264/389] Don't place another call if there's already one
 ongoing

The 'call' button doesn't turn into a hangup button as soon as there's
a call in the room, but we should have been doing this anyway.
---
 src/CallHandler.tsx         | 8 ++++++++
 src/i18n/strings/en_EN.json | 2 ++
 2 files changed, 10 insertions(+)

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 42a38c7a54..8621f441de 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -706,6 +706,14 @@ export default class CallHandler {
                         return;
                     }
 
+                    if (this.getCallForRoom(room.roomId)) {
+                        Modal.createTrackedDialog('Call Handler', 'Existing Call with user', ErrorDialog, {
+                            title: _t('Already in call'),
+                            description: _t("You're already in a call with this person."),
+                        });
+                        return;
+                    }
+
                     const members = room.getJoinedMembers();
                     if (members.length <= 1) {
                         Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index fa7f446b7c..a1999acb3b 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -58,6 +58,8 @@
     "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.",
     "Too Many Calls": "Too Many Calls",
     "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.",
+    "Already in call": "Already in call",
+    "You're already in a call with this person.": "You're already in a call with this person.",
     "You cannot place a call with yourself.": "You cannot place a call with yourself.",
     "Call in Progress": "Call in Progress",
     "A call is currently being placed!": "A call is currently being placed!",

From ae08f74336d3aa24988f7f3dba7d817cd5c96741 Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Wed, 3 Mar 2021 22:38:30 +0200
Subject: [PATCH 265/389] feat: improve "view source"

display encrypted and decrypted event source on the same dialog
keep only one "View Source" action on event context menu
---
 res/css/structures/_ViewSource.scss           | 11 ++++++-
 src/components/structures/ViewSource.js       | 32 ++++++++++++++-----
 .../views/context_menus/MessageContextMenu.js | 12 ++-----
 .../views/dialogs/RoomSettingsDialog.js       |  2 +-
 .../views/dialogs/UserSettingsDialog.js       |  2 +-
 src/i18n/strings/en_EN.json                   |  4 ++-
 src/i18n/strings/en_US.json                   |  4 ++-
 7 files changed, 44 insertions(+), 23 deletions(-)

diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss
index 421d1f03cd..0cb5dd8f3a 100644
--- a/res/css/structures/_ViewSource.scss
+++ b/res/css/structures/_ViewSource.scss
@@ -22,9 +22,18 @@ limitations under the License.
     float: right;
 }
 
-.mx_ViewSource_label_bottom {
+.mx_ViewSource_separator {
     clear: both;
     border-bottom: 1px solid #e5e5e5;
+    padding-top: 0.7em;
+    padding-bottom: 0.7em;
+}
+
+.mx_ViewSource_heading {
+    font-size: $font-17px;
+    font-weight: 400;
+    color: $primary-fg-color;
+    margin-top: 0.7em;
 }
 
 .mx_ViewSource pre {
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index 0b969784e5..866f2e0a0b 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -19,7 +19,7 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import SyntaxHighlight from '../views/elements/SyntaxHighlight';
-import {_t} from "../../languageHandler";
+import {_t, _td} from "../../languageHandler";
 import * as sdk from "../../index";
 
 
@@ -29,20 +29,36 @@ export default class ViewSource extends React.Component {
         onFinished: PropTypes.func.isRequired,
         roomId: PropTypes.string.isRequired,
         eventId: PropTypes.string.isRequired,
+        isEncrypted: PropTypes.bool.isRequired,
+        decryptedContent: PropTypes.object,
     };
 
     render() {
         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+
+        const DecryptedSection = <>
+            <div className="mx_ViewSource_separator" />
+            <div className="mx_ViewSource_heading">{_t("Decrypted event source")}</div>
+            <SyntaxHighlight className="json">
+                { JSON.stringify(this.props.decryptedContent, null, 2) }
+            </SyntaxHighlight>
+        </>;
+
+        const OriginalSection = <>
+            <div className="mx_ViewSource_separator" />
+            <div className="mx_ViewSource_heading">{_t("Original event source")}</div>
+            <SyntaxHighlight className="json">
+                { JSON.stringify(this.props.content, null, 2) }
+            </SyntaxHighlight>
+        </>;
+
         return (
             <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
-                <div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
-                <div className="mx_ViewSource_label_right">Event ID: { this.props.eventId }</div>
-                <div className="mx_ViewSource_label_bottom" />
-
                 <div className="mx_Dialog_content">
-                    <SyntaxHighlight className="json">
-                        { JSON.stringify(this.props.content, null, 2) }
-                    </SyntaxHighlight>
+                    <div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
+                    <div className="mx_ViewSource_label_left">Event ID: { this.props.eventId }</div>
+                    { this.props.isEncrypted && DecryptedSection }
+                    { OriginalSection }
                 </div>
             </BaseDialog>
         );
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index 6b871e4f24..b002d1ec62 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -130,6 +130,8 @@ export default class MessageContextMenu extends React.Component {
             roomId: ev.getRoomId(),
             eventId: ev.getId(),
             content: ev.event,
+            isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(),
+            decryptedContent: ev._clearEvent,
         }, 'mx_Dialog_viewsource');
         this.closeMenu();
     };
@@ -309,7 +311,6 @@ export default class MessageContextMenu extends React.Component {
         let cancelButton;
         let forwardButton;
         let pinButton;
-        let viewClearSourceButton;
         let unhidePreviewButton;
         let externalURLButton;
         let quoteButton;
@@ -389,14 +390,6 @@ export default class MessageContextMenu extends React.Component {
             </MenuItem>
         );
 
-        if (mxEvent.getType() !== mxEvent.getWireType()) {
-            viewClearSourceButton = (
-                <MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
-                    { _t('View Decrypted Source') }
-                </MenuItem>
-            );
-        }
-
         if (this.props.eventTileOps) {
             if (this.props.eventTileOps.isWidgetHidden()) {
                 unhidePreviewButton = (
@@ -481,7 +474,6 @@ export default class MessageContextMenu extends React.Component {
                 { forwardButton }
                 { pinButton }
                 { viewSourceButton }
-                { viewClearSourceButton }
                 { unhidePreviewButton }
                 { permalinkButton }
                 { quoteButton }
diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js
index 9d9313f08f..368f2aeccd 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.js
+++ b/src/components/views/dialogs/RoomSettingsDialog.js
@@ -116,7 +116,7 @@ export default class RoomSettingsDialog extends React.Component {
         return (
             <BaseDialog className='mx_RoomSettingsDialog' hasCancel={true}
                         onFinished={this.props.onFinished} title={_t("Room Settings - %(roomName)s", {roomName})}>
-                <div className='ms_SettingsDialog_content'>
+                <div className='mx_SettingsDialog_content'>
                     <TabbedView tabs={this._getTabs()} />
                 </div>
             </BaseDialog>
diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js
index 7164540aea..3291fa2387 100644
--- a/src/components/views/dialogs/UserSettingsDialog.js
+++ b/src/components/views/dialogs/UserSettingsDialog.js
@@ -155,7 +155,7 @@ export default class UserSettingsDialog extends React.Component {
         return (
             <BaseDialog className='mx_UserSettingsDialog' hasCancel={true}
                         onFinished={this.props.onFinished} title={_t("Settings")}>
-                <div className='ms_SettingsDialog_content'>
+                <div className='mx_SettingsDialog_content'>
                     <TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
                 </div>
             </BaseDialog>
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index fa7f446b7c..b777adcf0c 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2875,5 +2875,7 @@
     "Esc": "Esc",
     "Enter": "Enter",
     "Space": "Space",
-    "End": "End"
+    "End": "End",
+    "Decrypted event source": "Decrypted event source",
+    "Original event source": "Original event source"
 }
diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json
index a1275fb089..9dc6d18f8a 100644
--- a/src/i18n/strings/en_US.json
+++ b/src/i18n/strings/en_US.json
@@ -650,5 +650,7 @@
     "Error upgrading room": "Error upgrading room",
     "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
     "Changes the avatar of the current room": "Changes the avatar of the current room",
-    "Changes your avatar in all rooms": "Changes your avatar in all rooms"
+    "Changes your avatar in all rooms": "Changes your avatar in all rooms",
+    "Decrypted event source": "Decrypted event source",
+    "Original event source": "Original event source"
 }

From 0a1f372371a73736a720deface3564be172a9953 Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Wed, 3 Mar 2021 23:26:31 +0200
Subject: [PATCH 266/389] fix: lint

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

diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index 866f2e0a0b..b57efe1dd3 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -19,7 +19,7 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import SyntaxHighlight from '../views/elements/SyntaxHighlight';
-import {_t, _td} from "../../languageHandler";
+import {_t} from "../../languageHandler";
 import * as sdk from "../../index";
 
 

From 6d792cc08cec87e5ad6a856c5e4c9e593ac974df Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Wed, 3 Mar 2021 23:48:39 +0200
Subject: [PATCH 267/389] feat: use <details> to hide encrypted block

---
 res/css/structures/_ViewSource.scss     |  4 +++
 src/components/structures/ViewSource.js | 47 ++++++++++++++++---------
 2 files changed, 34 insertions(+), 17 deletions(-)

diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss
index 0cb5dd8f3a..0126c16599 100644
--- a/res/css/structures/_ViewSource.scss
+++ b/res/css/structures/_ViewSource.scss
@@ -43,3 +43,7 @@ limitations under the License.
     word-wrap: break-word;
     white-space: pre-wrap;
 }
+
+.mx_ViewSource_details {
+    margin-top: 0.8em;
+}
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index b57efe1dd3..ca6c0d4226 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -36,29 +36,42 @@ export default class ViewSource extends React.Component {
     render() {
         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
 
-        const DecryptedSection = <>
-            <div className="mx_ViewSource_separator" />
-            <div className="mx_ViewSource_heading">{_t("Decrypted event source")}</div>
-            <SyntaxHighlight className="json">
-                { JSON.stringify(this.props.decryptedContent, null, 2) }
-            </SyntaxHighlight>
-        </>;
-
-        const OriginalSection = <>
-            <div className="mx_ViewSource_separator" />
-            <div className="mx_ViewSource_heading">{_t("Original event source")}</div>
-            <SyntaxHighlight className="json">
-                { JSON.stringify(this.props.content, null, 2) }
-            </SyntaxHighlight>
-        </>;
+        let content;
+        if (this.props.isEncrypted) {
+            content = <>
+                <details open className="mx_ViewSource_details">
+                    <summary>
+                        <span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
+                    </summary>
+                    <SyntaxHighlight className="json">
+                        { JSON.stringify(this.props.decryptedContent, null, 2) }
+                    </SyntaxHighlight>
+                </details>
+                <details className="mx_ViewSource_details">
+                    <summary>
+                        <span className="mx_ViewSource_heading">{_t("Original event source")}</span>
+                    </summary>
+                    <SyntaxHighlight className="json">
+                        { JSON.stringify(this.props.content, null, 2) }
+                    </SyntaxHighlight>
+                </details>
+            </>;
+        } else {
+            content = <>
+                <div className="mx_ViewSource_heading">{_t("Original event source")}</div>
+                <SyntaxHighlight className="json">
+                    { JSON.stringify(this.props.content, null, 2) }
+                </SyntaxHighlight>
+            </>;
+        }
 
         return (
             <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
                 <div className="mx_Dialog_content">
                     <div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
                     <div className="mx_ViewSource_label_left">Event ID: { this.props.eventId }</div>
-                    { this.props.isEncrypted && DecryptedSection }
-                    { OriginalSection }
+                    <div className="mx_ViewSource_separator" />
+                    { content }
                 </div>
             </BaseDialog>
         );

From 725162ee0012c85111859fa54c9058462945e761 Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Thu, 4 Mar 2021 00:09:00 +0200
Subject: [PATCH 268/389] fix: i18n strings

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

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b777adcf0c..c3752d7942 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2396,7 +2396,6 @@
     "Cancel Sending": "Cancel Sending",
     "Forward Message": "Forward Message",
     "Pin Message": "Pin Message",
-    "View Decrypted Source": "View Decrypted Source",
     "Unhide Preview": "Unhide Preview",
     "Share Permalink": "Share Permalink",
     "Share Message": "Share Message",
@@ -2652,6 +2651,8 @@
     "User menu": "User menu",
     "Community and user menu": "Community and user menu",
     "Could not load user profile": "Could not load user profile",
+    "Decrypted event source": "Decrypted event source",
+    "Original event source": "Original event source",
     "Verify this login": "Verify this login",
     "Session verified": "Session verified",
     "Failed to send email": "Failed to send email",
@@ -2875,7 +2876,5 @@
     "Esc": "Esc",
     "Enter": "Enter",
     "Space": "Space",
-    "End": "End",
-    "Decrypted event source": "Decrypted event source",
-    "Original event source": "Original event source"
+    "End": "End"
 }
diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json
index 9dc6d18f8a..a1275fb089 100644
--- a/src/i18n/strings/en_US.json
+++ b/src/i18n/strings/en_US.json
@@ -650,7 +650,5 @@
     "Error upgrading room": "Error upgrading room",
     "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.",
     "Changes the avatar of the current room": "Changes the avatar of the current room",
-    "Changes your avatar in all rooms": "Changes your avatar in all rooms",
-    "Decrypted event source": "Decrypted event source",
-    "Original event source": "Original event source"
+    "Changes your avatar in all rooms": "Changes your avatar in all rooms"
 }

From 1cb19554eb3a151706b6b964f0c42f61b58c7a23 Mon Sep 17 00:00:00 2001
From: Ayush Kumar <2580ayush2580@gmail.com>
Date: Thu, 4 Mar 2021 14:03:02 +0530
Subject: [PATCH 269/389] Fix Bottom border of state counters is white on the
 dark theme

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

diff --git a/res/css/views/rooms/_AuxPanel.scss b/res/css/views/rooms/_AuxPanel.scss
index 34ef5e01d4..17a6294bf0 100644
--- a/res/css/views/rooms/_AuxPanel.scss
+++ b/res/css/views/rooms/_AuxPanel.scss
@@ -17,7 +17,7 @@ limitations under the License.
 .m_RoomView_auxPanel_stateViews {
     padding: 5px;
     padding-left: 19px;
-    border-bottom: 1px solid #e5e5e5;
+    border-bottom: 1px solid $primary-hairline-color;
 }
 
 .m_RoomView_auxPanel_stateViews_span a {

From f2d2a048e184f597acd5d605c83944e63ecdecb1 Mon Sep 17 00:00:00 2001
From: Jason Robinson <mail@jasonrobinson.me>
Date: Thu, 4 Mar 2021 11:07:14 +0200
Subject: [PATCH 270/389] Ensure HostSignupDialog border colour matches light
 theme

Ensure dialog borders are always white as the HostSignupDialog
does not yet support dark mode or theming in general.
In the future we might want to pass the theme to the called
iframe, should some hosting provider have that need.
---
 res/css/views/dialogs/_HostSignupDialog.scss | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/res/css/views/dialogs/_HostSignupDialog.scss b/res/css/views/dialogs/_HostSignupDialog.scss
index 1378ac9053..ac4bc41951 100644
--- a/res/css/views/dialogs/_HostSignupDialog.scss
+++ b/res/css/views/dialogs/_HostSignupDialog.scss
@@ -19,6 +19,11 @@ limitations under the License.
     max-width: 580px;
     height: 80vh;
     max-height: 600px;
+    // Ensure dialog borders are always white as the HostSignupDialog
+    // does not yet support dark mode or theming in general.
+    // In the future we might want to pass the theme to the called
+    // iframe, should some hosting provider have that need.
+    background-color: #ffffff;
 
     .mx_HostSignupDialog_info {
         text-align: center;

From 63944b9f6da84dc4b385cb3f7f3b995cabab56e4 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 Mar 2021 12:22:31 +0000
Subject: [PATCH 271/389] Add the new file

---
 src/Livestream.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 52 insertions(+)
 create mode 100644 src/Livestream.ts

diff --git a/src/Livestream.ts b/src/Livestream.ts
new file mode 100644
index 0000000000..d4bed63dbd
--- /dev/null
+++ b/src/Livestream.ts
@@ -0,0 +1,52 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { ClientWidgetApi } from "matrix-widget-api";
+import { MatrixClientPeg } from "./MatrixClientPeg";
+import SdkConfig from "./SdkConfig";
+import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
+
+export function getConfigLivestreamUrl() {
+    return SdkConfig.get()["audioStreamUrl"];
+}
+
+async function createLiveStream(roomId: string) {
+    const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
+
+    const url = getConfigLivestreamUrl() + "/createStream";
+
+    const response = await window.fetch(url, {
+        method: 'POST',
+        headers: {
+            "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+            room_id: roomId,
+            openid_token: openIdToken,
+        }),
+    });
+
+    const respBody = response.json();
+    return respBody['stream_id'];
+}
+
+export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) {
+    const streamId = await createLiveStream(roomId);
+
+    widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, {
+        rtmpStreamKey: 'audioStream:' + streamId,
+    });
+}

From ab4220b20d98d4e47574f945baee64bbf2ec3b48 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 4 Mar 2021 13:04:58 +0000
Subject: [PATCH 272/389] Defer auto-joining within spaces and switch to using
 `suggested`

---
 .../structures/SpaceRoomDirectory.tsx         | 33 ++++++++++---------
 src/i18n/strings/en_EN.json                   |  2 +-
 src/stores/SpaceStore.tsx                     |  3 +-
 3 files changed, 20 insertions(+), 18 deletions(-)

diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index 7f7b9dbb99..0a59ac0195 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -64,6 +64,7 @@ export interface ISpaceSummaryEvent {
     state_key: string;
     content: {
         order?: string;
+        suggested?: boolean;
         auto_join?: boolean;
         via?: string;
     };
@@ -91,7 +92,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
     const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
 
     const evContent = event?.getContent();
-    const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
+    const [suggested, _setSuggested] = useState(evContent?.suggested);
     const [removed, _setRemoved] = useState(!evContent?.via);
 
     const cli = MatrixClientPeg.get();
@@ -102,12 +103,12 @@ const SubSpace: React.FC<ISubspaceProps> = ({
     let actions;
     if (editing && queueAction) {
         if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
-            const setAutoJoin = () => {
-                _setAutoJoin(v => {
+            const setSuggested = () => {
+                _setSuggested(v => {
                     queueAction({
                         event,
                         removed,
-                        autoJoin: !v,
+                        suggested: !v,
                     });
                     return !v;
                 });
@@ -118,7 +119,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
                     queueAction({
                         event,
                         removed: !v,
-                        autoJoin,
+                        suggested,
                     });
                     return !v;
                 });
@@ -131,7 +132,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
             } else {
                 actions = <React.Fragment>
                     <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
-                    <StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
+                    <StyledCheckbox checked={suggested} onChange={setSuggested} />
                 </React.Fragment>;
             }
         } else {
@@ -180,8 +181,8 @@ const SubSpace: React.FC<ISubspaceProps> = ({
 
 interface IAction {
     event: MatrixEvent;
+    suggested: boolean;
     removed: boolean;
-    autoJoin: boolean;
 }
 
 interface IRoomTileProps {
@@ -197,7 +198,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
     const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
 
     const evContent = event?.getContent();
-    const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
+    const [suggested, _setSuggested] = useState(evContent?.suggested);
     const [removed, _setRemoved] = useState(!evContent?.via);
 
     const cli = MatrixClientPeg.get();
@@ -207,12 +208,12 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
     let actions;
     if (editing && queueAction) {
         if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
-            const setAutoJoin = () => {
-                _setAutoJoin(v => {
+            const setSuggested = () => {
+                _setSuggested(v => {
                     queueAction({
                         event,
                         removed,
-                        autoJoin: !v,
+                        suggested: !v,
                     });
                     return !v;
                 });
@@ -223,7 +224,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
                     queueAction({
                         event,
                         removed: !v,
-                        autoJoin,
+                        suggested,
                     });
                     return !v;
                 });
@@ -236,7 +237,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
             } else {
                 actions = <React.Fragment>
                     <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
-                    <StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
+                    <StyledCheckbox checked={suggested} onChange={setSuggested} />
                 </React.Fragment>;
             }
         } else {
@@ -441,10 +442,10 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
 
         const onSaveButtonClicked = () => {
             // TODO setBusy
-            pendingActions.current.forEach(({event, autoJoin, removed}) => {
+            pendingActions.current.forEach(({event, suggested, removed}) => {
                 const content = {
                     ...event.getContent(),
-                    auto_join: autoJoin,
+                    suggested,
                 };
 
                 if (removed) {
@@ -459,7 +460,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
         if (isEditing) {
             adminButton = <React.Fragment>
                 <FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
-                <span>{ _t("All users join by default") }</span>
+                <span>{ _t("Promoted to users") }</span>
             </React.Fragment>;
         } else {
             adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e256e6bb2f..7b680b6590 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2596,7 +2596,7 @@
     "You're in this space": "You're in this space",
     "You're in this room": "You're in this room",
     "Save changes": "Save changes",
-    "All users join by default": "All users join by default",
+    "Promoted to users": "Promoted to users",
     "Manage rooms": "Manage rooms",
     "Find a room...": "Find a room...",
     "Accept Invite": "Accept Invite",
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index 8e0066da91..c334144d70 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -108,9 +108,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
         }
     }
 
-    public addRoomToSpace(space: Room, roomId: string, via: string[], autoJoin = false) {
+    public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) {
         return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, {
             via,
+            suggested,
             auto_join: autoJoin,
         }, roomId);
     }

From dd792c3d7329400bf67c95cf671190203d54afd7 Mon Sep 17 00:00:00 2001
From: Ayush Kumar <2580ayush2580@gmail.com>
Date: Thu, 4 Mar 2021 19:24:35 +0530
Subject: [PATCH 273/389] Fix Clicking on the avatar for opening member info
 requires pixel-perfect accuracy

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

diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss
index 903fabc8fd..818509785b 100644
--- a/res/css/views/rooms/_GroupLayout.scss
+++ b/res/css/views/rooms/_GroupLayout.scss
@@ -21,7 +21,7 @@ $left-gutter: 64px;
     .mx_EventTile {
         > .mx_SenderProfile {
             line-height: $font-20px;
-            padding-left: $left-gutter;
+            margin-left: $left-gutter;
         }
 
         > .mx_EventTile_line {

From 0f1b7a001e1b943ff28acc664b009264cc7fd588 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 Mar 2021 17:52:49 +0000
Subject: [PATCH 274/389] Better error handling for streams

Also use older youtubeStreamKey as it appears our jitsi doesn't
support the newer one.
---
 src/Livestream.ts                                 |  6 +++---
 .../views/context_menus/WidgetContextMenu.tsx     | 15 +++++++++++++--
 src/i18n/strings/en_EN.json                       |  2 ++
 3 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/src/Livestream.ts b/src/Livestream.ts
index d4bed63dbd..cd8cdea179 100644
--- a/src/Livestream.ts
+++ b/src/Livestream.ts
@@ -39,14 +39,14 @@ async function createLiveStream(roomId: string) {
         }),
     });
 
-    const respBody = response.json();
+    const respBody = await response.json();
     return respBody['stream_id'];
 }
 
 export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) {
     const streamId = await createLiveStream(roomId);
 
-    widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, {
-        rtmpStreamKey: 'audioStream:' + streamId,
+    await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, {
+        rtmpStreamKey: 'rtmp://audiostream.dummy/' + streamId,
     });
 }
diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx
index e7d1c02c66..0503df038a 100644
--- a/src/components/views/context_menus/WidgetContextMenu.tsx
+++ b/src/components/views/context_menus/WidgetContextMenu.tsx
@@ -28,6 +28,7 @@ import dis from "../../../dispatcher/dispatcher";
 import SettingsStore from "../../../settings/SettingsStore";
 import Modal from "../../../Modal";
 import QuestionDialog from "../dialogs/QuestionDialog";
+import ErrorDialog from "../dialogs/ErrorDialog";
 import {WidgetType} from "../../../widgets/WidgetType";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
@@ -57,8 +58,18 @@ const WidgetContextMenu: React.FC<IProps> = ({
 
     let streamAudioStreamButton;
     if (getConfigLivestreamUrl() && (app.type === "m.jitsi" || app.type === "jitsi")) {
-        const onStreamAudioClick = () => {
-            startJitsiAudioLivestream(widgetMessaging, roomId);
+        const onStreamAudioClick = async () => {
+            try {
+                await startJitsiAudioLivestream(widgetMessaging, roomId);
+            } catch (err) {
+                console.log("Failed to start livestream", err);
+                // XXX: won't i18n well, but looks like widget api only support 'message'?
+                const message = err.message || _t("Unable to start audio streaming.");
+                Modal.createTrackedDialog('WidgetContext Menu', 'Livestream failed', ErrorDialog, {
+                    title: _t('Failed to start livestream'),
+                    description: message,
+                });
+            }
             onFinished();
         };
         streamAudioStreamButton = <IconizedContextMenuOption
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 7242ed7de6..f33c4368ef 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2357,6 +2357,8 @@
     "Set status": "Set status",
     "Set a new status...": "Set a new status...",
     "View Community": "View Community",
+    "Failed to start livestream": "Failed to start livestream",
+    "Unable to start audio streaming.": "Unable to start audio streaming.",
     "Start audio stream": "Start audio stream",
     "Take a picture": "Take a picture",
     "Delete Widget": "Delete Widget",

From aaf653dd8f0f616792569b888224958d9be99fba Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 4 Mar 2021 17:58:43 +0000
Subject: [PATCH 275/389] 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 f33c4368ef..2183a0c68f 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2357,8 +2357,8 @@
     "Set status": "Set status",
     "Set a new status...": "Set a new status...",
     "View Community": "View Community",
-    "Failed to start livestream": "Failed to start livestream",
     "Unable to start audio streaming.": "Unable to start audio streaming.",
+    "Failed to start livestream": "Failed to start livestream",
     "Start audio stream": "Start audio stream",
     "Take a picture": "Take a picture",
     "Delete Widget": "Delete Widget",

From af5cfff51db48c4a96c5393a25c76675839e864d Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Thu, 4 Mar 2021 23:17:29 +0200
Subject: [PATCH 276/389] feat: edit button on View Source dialog

reuse component SendCustomEvent
swap it in place in the View Source dialog
the Back button takes you to the View Source dialog, not the DevTools dialog
do not display the flip toggle box for changing between State Event and Normal Event
---
 src/components/structures/ViewSource.js       | 180 ++++++++++++++----
 .../views/context_menus/MessageContextMenu.js |  14 +-
 .../views/dialogs/DevtoolsDialog.js           |   7 +-
 3 files changed, 152 insertions(+), 49 deletions(-)

diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index ca6c0d4226..7fe862cff5 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -16,12 +16,13 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
-import PropTypes from 'prop-types';
-import SyntaxHighlight from '../views/elements/SyntaxHighlight';
-import {_t} from "../../languageHandler";
+import React from "react";
+import PropTypes from "prop-types";
+import SyntaxHighlight from "../views/elements/SyntaxHighlight";
+import { _t } from "../../languageHandler";
 import * as sdk from "../../index";
-
+import MatrixClientContext from "../../contexts/MatrixClientContext";
+import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
 
 export default class ViewSource extends React.Component {
     static propTypes = {
@@ -31,48 +32,157 @@ export default class ViewSource extends React.Component {
         eventId: PropTypes.string.isRequired,
         isEncrypted: PropTypes.bool.isRequired,
         decryptedContent: PropTypes.object,
+        event: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
     };
 
+    constructor(props) {
+        super(props);
+
+        this.state = {
+            editComponent: null,
+        };
+    }
+
+    onBack() {
+        this.setState({ editComponent: null });
+    }
+
+    editEvent() {
+        const isStateEvent = this.props.event.isState();
+        console.log("isStateEvent", isStateEvent);
+        if (isStateEvent) {
+            this.setState({
+                editComponent: (
+                    <MatrixClientContext.Consumer>
+                        {(cli) => (
+                            <SendCustomEvent
+                                room={cli.getRoom(this.props.roomId)}
+                                forceStateEvent={true}
+                                onBack={() => this.onBack()}
+                                inputs={{
+                                    eventType: this.props.event.getType(),
+                                    evContent: JSON.stringify(
+                                        this.props.event.getContent(),
+                                        null,
+                                        "\t"
+                                    ),
+                                    stateKey: this.props.event.getStateKey(),
+                                }}
+                            />
+                        )}
+                    </MatrixClientContext.Consumer>
+                ),
+            });
+        } else {
+            // send an edit-message event
+            // prefill the "m.new_content" field
+            const originalContent = this.props.event.getContent();
+            const originalEventId = this.props.eventId;
+            const content = {
+                ...originalContent,
+                "m.new_content": originalContent,
+                "m.relates_to": {
+                    rel_type: "m.replace",
+                    event_id: originalEventId,
+                },
+            };
+            this.setState({
+                editComponent: (
+                    <MatrixClientContext.Consumer>
+                        {(cli) => (
+                            <SendCustomEvent
+                                room={cli.getRoom(this.props.roomId)}
+                                forceStateEvent={false}
+                                forceGeneralEvent={true}
+                                onBack={() => this.onBack()}
+                                inputs={{
+                                    eventType: this.props.event.getType(),
+                                    evContent: JSON.stringify(
+                                        content,
+                                        null,
+                                        "\t"
+                                    ),
+                                }}
+                            />
+                        )}
+                    </MatrixClientContext.Consumer>
+                ),
+            });
+        }
+    }
+
     render() {
-        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+        const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
 
         let content;
         if (this.props.isEncrypted) {
-            content = <>
-                <details open className="mx_ViewSource_details">
-                    <summary>
-                        <span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
-                    </summary>
-                    <SyntaxHighlight className="json">
-                        { JSON.stringify(this.props.decryptedContent, null, 2) }
-                    </SyntaxHighlight>
-                </details>
-                <details className="mx_ViewSource_details">
-                    <summary>
-                        <span className="mx_ViewSource_heading">{_t("Original event source")}</span>
-                    </summary>
-                    <SyntaxHighlight className="json">
-                        { JSON.stringify(this.props.content, null, 2) }
-                    </SyntaxHighlight>
-                </details>
-            </>;
+            content = (
+                <>
+                    <details open className="mx_ViewSource_details">
+                        <summary>
+                            <span className="mx_ViewSource_heading">
+                                {_t("Decrypted event source")}
+                            </span>
+                        </summary>
+                        <SyntaxHighlight className="json">
+                            {JSON.stringify(
+                                this.props.decryptedContent,
+                                null,
+                                2
+                            )}
+                        </SyntaxHighlight>
+                    </details>
+                    <details className="mx_ViewSource_details">
+                        <summary>
+                            <span className="mx_ViewSource_heading">
+                                {_t("Original event source")}
+                            </span>
+                        </summary>
+                        <SyntaxHighlight className="json">
+                            {JSON.stringify(this.props.content, null, 2)}
+                        </SyntaxHighlight>
+                    </details>
+                </>
+            );
         } else {
-            content = <>
-                <div className="mx_ViewSource_heading">{_t("Original event source")}</div>
-                <SyntaxHighlight className="json">
-                    { JSON.stringify(this.props.content, null, 2) }
-                </SyntaxHighlight>
-            </>;
+            content = (
+                <>
+                    <div className="mx_ViewSource_heading">
+                        {_t("Original event source")}
+                    </div>
+                    <SyntaxHighlight className="json">
+                        {JSON.stringify(this.props.content, null, 2)}
+                    </SyntaxHighlight>
+                </>
+            );
         }
 
+        const isEditing = this.state.editComponent !== null;
+        console.log(isEditing);
+
         return (
-            <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
-                <div className="mx_Dialog_content">
-                    <div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
-                    <div className="mx_ViewSource_label_left">Event ID: { this.props.eventId }</div>
+            <BaseDialog
+                className="mx_ViewSource"
+                onFinished={this.props.onFinished}
+                title={_t("View Source")}
+            >
+                <div>
+                    <div className="mx_ViewSource_label_left">
+                        Room ID: {this.props.roomId}
+                    </div>
+                    <div className="mx_ViewSource_label_left">
+                        Event ID: {this.props.eventId}
+                    </div>
                     <div className="mx_ViewSource_separator" />
-                    { content }
+                    {isEditing ? this.state.editComponent : content}
                 </div>
+                {!isEditing && (
+                    <div className="mx_Dialog_buttons">
+                        <button onClick={() => this.editEvent()}>
+                            {_t("Edit")}
+                        </button>
+                    </div>
+                )}
             </BaseDialog>
         );
     }
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index b002d1ec62..a1c111b19c 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -130,20 +130,10 @@ export default class MessageContextMenu extends React.Component {
             roomId: ev.getRoomId(),
             eventId: ev.getId(),
             content: ev.event,
+            event: ev,
             isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(),
-            decryptedContent: ev._clearEvent,
-        }, 'mx_Dialog_viewsource');
-        this.closeMenu();
-    };
-
-    onViewClearSourceClick = () => {
-        const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
-        const ViewSource = sdk.getComponent('structures.ViewSource');
-        Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
-            roomId: ev.getRoomId(),
-            eventId: ev.getId(),
             // FIXME: _clearEvent is private
-            content: ev._clearEvent,
+            decryptedContent: ev._clearEvent,
         }, 'mx_Dialog_viewsource');
         this.closeMenu();
     };
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index 814378bb51..5d571461fc 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -73,13 +73,14 @@ class GenericEditor extends React.PureComponent {
     }
 }
 
-class SendCustomEvent extends GenericEditor {
+export class SendCustomEvent extends GenericEditor {
     static getLabel() { return _t('Send Custom Event'); }
 
     static propTypes = {
         onBack: PropTypes.func.isRequired,
         room: PropTypes.instanceOf(Room).isRequired,
         forceStateEvent: PropTypes.bool,
+        forceGeneralEvent: PropTypes.bool,
         inputs: PropTypes.object,
     };
 
@@ -140,6 +141,8 @@ class SendCustomEvent extends GenericEditor {
             </div>;
         }
 
+        const showTglFlip = !this.state.message && !this.props.forceStateEvent && !this.props.forceGeneralEvent;
+        
         return <div>
             <div className="mx_DevTools_content">
                 <div className="mx_DevTools_eventTypeStateKeyGroup">
@@ -155,7 +158,7 @@ class SendCustomEvent extends GenericEditor {
             <div className="mx_Dialog_buttons">
                 <button onClick={this.onBack}>{ _t('Back') }</button>
                 { !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
-                { !this.state.message && !this.props.forceStateEvent && <div style={{float: "right"}}>
+                { showTglFlip && <div style={{float: "right"}}>
                     <input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
                     <label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
                 </div> }

From 288d98daede9f1ea6b1045e6de930c61f5f14c0e Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Fri, 5 Mar 2021 00:07:59 +0200
Subject: [PATCH 277/389] chore: format, lint

---
 src/components/structures/ViewSource.js       | 58 ++++---------------
 .../views/dialogs/DevtoolsDialog.js           |  2 +-
 2 files changed, 13 insertions(+), 47 deletions(-)

diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index 7fe862cff5..a31876ea76 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -61,11 +61,7 @@ export default class ViewSource extends React.Component {
                                 onBack={() => this.onBack()}
                                 inputs={{
                                     eventType: this.props.event.getType(),
-                                    evContent: JSON.stringify(
-                                        this.props.event.getContent(),
-                                        null,
-                                        "\t"
-                                    ),
+                                    evContent: JSON.stringify(this.props.event.getContent(), null, "\t"),
                                     stateKey: this.props.event.getStateKey(),
                                 }}
                             />
@@ -97,11 +93,7 @@ export default class ViewSource extends React.Component {
                                 onBack={() => this.onBack()}
                                 inputs={{
                                     eventType: this.props.event.getType(),
-                                    evContent: JSON.stringify(
-                                        content,
-                                        null,
-                                        "\t"
-                                    ),
+                                    evContent: JSON.stringify(content, null, "\t"),
                                 }}
                             />
                         )}
@@ -120,39 +112,23 @@ export default class ViewSource extends React.Component {
                 <>
                     <details open className="mx_ViewSource_details">
                         <summary>
-                            <span className="mx_ViewSource_heading">
-                                {_t("Decrypted event source")}
-                            </span>
+                            <span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
                         </summary>
-                        <SyntaxHighlight className="json">
-                            {JSON.stringify(
-                                this.props.decryptedContent,
-                                null,
-                                2
-                            )}
-                        </SyntaxHighlight>
+                        <SyntaxHighlight className="json">{JSON.stringify(this.props.decryptedContent, null, 2)}</SyntaxHighlight>
                     </details>
                     <details className="mx_ViewSource_details">
                         <summary>
-                            <span className="mx_ViewSource_heading">
-                                {_t("Original event source")}
-                            </span>
+                            <span className="mx_ViewSource_heading">{_t("Original event source")}</span>
                         </summary>
-                        <SyntaxHighlight className="json">
-                            {JSON.stringify(this.props.content, null, 2)}
-                        </SyntaxHighlight>
+                        <SyntaxHighlight className="json">{JSON.stringify(this.props.content, null, 2)}</SyntaxHighlight>
                     </details>
                 </>
             );
         } else {
             content = (
                 <>
-                    <div className="mx_ViewSource_heading">
-                        {_t("Original event source")}
-                    </div>
-                    <SyntaxHighlight className="json">
-                        {JSON.stringify(this.props.content, null, 2)}
-                    </SyntaxHighlight>
+                    <div className="mx_ViewSource_heading">{_t("Original event source")}</div>
+                    <SyntaxHighlight className="json">{JSON.stringify(this.props.content, null, 2)}</SyntaxHighlight>
                 </>
             );
         }
@@ -161,26 +137,16 @@ export default class ViewSource extends React.Component {
         console.log(isEditing);
 
         return (
-            <BaseDialog
-                className="mx_ViewSource"
-                onFinished={this.props.onFinished}
-                title={_t("View Source")}
-            >
+            <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
                 <div>
-                    <div className="mx_ViewSource_label_left">
-                        Room ID: {this.props.roomId}
-                    </div>
-                    <div className="mx_ViewSource_label_left">
-                        Event ID: {this.props.eventId}
-                    </div>
+                    <div className="mx_ViewSource_label_left">Room ID: {this.props.roomId}</div>
+                    <div className="mx_ViewSource_label_left">Event ID: {this.props.eventId}</div>
                     <div className="mx_ViewSource_separator" />
                     {isEditing ? this.state.editComponent : content}
                 </div>
                 {!isEditing && (
                     <div className="mx_Dialog_buttons">
-                        <button onClick={() => this.editEvent()}>
-                            {_t("Edit")}
-                        </button>
+                        <button onClick={() => this.editEvent()}>{_t("Edit")}</button>
                     </div>
                 )}
             </BaseDialog>
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index 5d571461fc..82f2df6534 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -142,7 +142,7 @@ export class SendCustomEvent extends GenericEditor {
         }
 
         const showTglFlip = !this.state.message && !this.props.forceStateEvent && !this.props.forceGeneralEvent;
-        
+
         return <div>
             <div className="mx_DevTools_content">
                 <div className="mx_DevTools_eventTypeStateKeyGroup">

From 5d6e3d5711b56eddb764e0ee8924b20c4e5ed5e2 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 4 Mar 2021 20:07:48 -0700
Subject: [PATCH 278/389] UI refresh for uploaded files

Fixes https://github.com/vector-im/element-web/issues/16557
Fixes https://github.com/vector-im/element-web/issues/9482 (technically)

There's two changes in this:
1. The actual file body in the timeline now has a placeholder thing.
2. We're intentionally dropping all the "Travis uploaded a file" sender profile states.
---
 res/css/views/messages/_MFileBody.scss        | 43 +++++++++++++++++++
 res/themes/dark/css/_dark.scss                |  4 ++
 res/themes/legacy-dark/css/_legacy-dark.scss  |  4 ++
 .../legacy-light/css/_legacy-light.scss       |  4 ++
 res/themes/light/css/_light.scss              |  4 ++
 src/components/views/messages/MAudioBody.js   |  2 +-
 src/components/views/messages/MFileBody.js    | 26 ++++++++++-
 src/components/views/messages/MImageBody.js   |  2 +-
 src/components/views/messages/MVideoBody.tsx  |  2 +-
 .../views/messages/SenderProfile.js           | 10 +----
 src/components/views/rooms/EventTile.js       |  7 +--
 src/i18n/strings/en_EN.json                   |  3 --
 12 files changed, 88 insertions(+), 23 deletions(-)

diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss
index 6cbce68745..e219c0c5e4 100644
--- a/res/css/views/messages/_MFileBody.scss
+++ b/res/css/views/messages/_MFileBody.scss
@@ -45,3 +45,46 @@ limitations under the License.
      * big the content of the iframe is. */
     height: 1.5em;
 }
+
+.mx_MFileBody_info {
+    background-color: $message-body-panel-bg-color;
+    border-radius: 4px;
+    width: 270px;
+    padding: 8px;
+    color: $message-body-panel-fg-color;
+
+    .mx_MFileBody_info_icon {
+        background-color: $message-body-panel-icon-bg-color;
+        border-radius: 20px;
+        display: inline-block;
+        width: 32px;
+        height: 32px;
+        position: relative;
+        vertical-align: middle;
+        margin-right: 12px;
+
+        &::before {
+            content: '';
+            mask-repeat: no-repeat;
+            mask-position: center;
+            mask-size: cover;
+            mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
+            background-color: $message-body-panel-fg-color;
+            width: 13px;
+            height: 15px;
+
+            position: absolute;
+            top: 8px;
+            left: 9px;
+        }
+    }
+
+    .mx_MFileBody_info_filename {
+        text-overflow: ellipsis;
+        overflow: hidden;
+        white-space: nowrap;
+        display: inline-block;
+        width: calc(100% - 32px - 12px); // 32px icon, 12px margin on the icon
+        vertical-align: middle;
+    }
+}
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 0de5e69782..a4648d2051 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -203,6 +203,10 @@ $breadcrumb-placeholder-bg-color: #272c35;
 
 $user-tile-hover-bg-color: $header-panel-bg-color;
 
+$message-body-panel-bg-color: #21262c82;
+$message-body-panel-icon-bg-color: #8e99a4;
+$message-body-panel-fg-color: $primary-fg-color;
+
 // Appearance tab colors
 $appearance-tab-border-color: $room-highlight-color;
 
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 8c5f20178b..8752e41d18 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -198,6 +198,10 @@ $breadcrumb-placeholder-bg-color: #272c35;
 
 $user-tile-hover-bg-color: $header-panel-bg-color;
 
+$message-body-panel-bg-color: #21262c82;
+$message-body-panel-icon-bg-color: #8e99a4;
+$message-body-panel-fg-color: $primary-fg-color;
+
 // Appearance tab colors
 $appearance-tab-border-color: $room-highlight-color;
 
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 3ba10a68ea..58b75f32aa 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -322,6 +322,10 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
 
 $user-tile-hover-bg-color: $header-panel-bg-color;
 
+$message-body-panel-bg-color: #e3e8f082;
+$message-body-panel-icon-bg-color: #ffffff;
+$message-body-panel-fg-color: $muted-fg-color;
+
 // FontSlider colors
 $appearance-tab-border-color: $input-darker-bg-color;
 
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 76bf2ddc21..fd2bfe4628 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -323,6 +323,10 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
 
 $user-tile-hover-bg-color: $header-panel-bg-color;
 
+$message-body-panel-bg-color: #e3e8f082;
+$message-body-panel-icon-bg-color: #ffffff;
+$message-body-panel-fg-color: $muted-fg-color;
+
 // FontSlider colors
 $appearance-tab-border-color: $input-darker-bg-color;
 
diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js
index 37f85a108f..587dee4513 100644
--- a/src/components/views/messages/MAudioBody.js
+++ b/src/components/views/messages/MAudioBody.js
@@ -105,7 +105,7 @@ export default class MAudioBody extends React.Component {
         return (
             <span className="mx_MAudioBody">
                 <audio src={contentUrl} controls />
-                <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
+                <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
             </span>
         );
     }
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index 770cd4fff3..676f0b7986 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -126,6 +126,12 @@ export default class MFileBody extends React.Component {
         onHeightChanged: PropTypes.func,
         /* the shape of the tile, used */
         tileShape: PropTypes.string,
+        /* whether or not to show the default placeholder for the file. Defaults to true. */
+        showGenericPlaceholder: PropTypes.bool,
+    };
+
+    static defaultProps = {
+        showGenericPlaceholder: true,
     };
 
     constructor(props) {
@@ -145,9 +151,10 @@ export default class MFileBody extends React.Component {
      * link text.
      *
      * @param {Object} content The "content" key of the matrix event.
+     * @param {boolean} withSize Whether to include size information. Default true.
      * @return {string} the human readable link text for the attachment.
      */
-    presentableTextForFile(content) {
+    presentableTextForFile(content, withSize = true) {
         let linkText = _t("Attachment");
         if (content.body && content.body.length > 0) {
             // The content body should be the name of the file including a
@@ -155,7 +162,7 @@ export default class MFileBody extends React.Component {
             linkText = content.body;
         }
 
-        if (content.info && content.info.size) {
+        if (content.info && content.info.size && withSize) {
             // If we know the size of the file then add it as human readable
             // string to the end of the link text so that the user knows how
             // big a file they are downloading.
@@ -218,6 +225,16 @@ export default class MFileBody extends React.Component {
         const fileSize = content.info ? content.info.size : null;
         const fileType = content.info ? content.info.mimetype : "application/octet-stream";
 
+        let placeholder = null;
+        if (this.props.showGenericPlaceholder) {
+            placeholder = (
+                <div className="mx_MFileBody_info">
+                    <span className="mx_MFileBody_info_icon" />
+                    <span className="mx_MFileBody_info_filename">{this.presentableTextForFile(content, false)}</span>
+                </div>
+            );
+        }
+
         if (isEncrypted) {
             if (this.state.decryptedBlob === null) {
                 // Need to decrypt the attachment
@@ -248,6 +265,7 @@ export default class MFileBody extends React.Component {
                 // but it is not guaranteed between various browsers' settings.
                 return (
                     <span className="mx_MFileBody">
+                        {placeholder}
                         <div className="mx_MFileBody_download">
                             <AccessibleButton onClick={decrypt}>
                                 { _t("Decrypt %(text)s", { text: text }) }
@@ -278,6 +296,7 @@ export default class MFileBody extends React.Component {
             // If the attachment is encrypted then put the link inside an iframe.
             return (
                 <span className="mx_MFileBody">
+                    {placeholder}
                     <div className="mx_MFileBody_download">
                         <div style={{display: "none"}}>
                             { /*
@@ -346,6 +365,7 @@ export default class MFileBody extends React.Component {
             if (this.props.tileShape === "file_grid") {
                 return (
                     <span className="mx_MFileBody">
+                        {placeholder}
                         <div className="mx_MFileBody_download">
                             <a className="mx_MFileBody_downloadLink" {...downloadProps}>
                                 { fileName }
@@ -359,6 +379,7 @@ export default class MFileBody extends React.Component {
             } else {
                 return (
                     <span className="mx_MFileBody">
+                        {placeholder}
                         <div className="mx_MFileBody_download">
                             <a {...downloadProps}>
                                 <img src={tintedDownloadImageURL} width="12" height="14" ref={this._downloadImage} />
@@ -371,6 +392,7 @@ export default class MFileBody extends React.Component {
         } else {
             const extra = text ? (': ' + text) : '';
             return <span className="mx_MFileBody">
+                {placeholder}
                 { _t("Invalid file%(extra)s", { extra: extra }) }
             </span>;
         }
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index a8cdc17abf..771d12accd 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -452,7 +452,7 @@ export default class MImageBody extends React.Component {
 
     // Overidden by MStickerBody
     getFileBody() {
-        return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />;
+        return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
     }
 
     render() {
diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx
index 9628f11809..ce4a4eda76 100644
--- a/src/components/views/messages/MVideoBody.tsx
+++ b/src/components/views/messages/MVideoBody.tsx
@@ -243,7 +243,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
                     onPlay={this.videoOnPlay}
                 >
                 </video>
-                <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
+                <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
             </span>
         );
     }
diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js
index afe2d6d118..e2bb1ff38d 100644
--- a/src/components/views/messages/SenderProfile.js
+++ b/src/components/views/messages/SenderProfile.js
@@ -25,7 +25,6 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
 export default class SenderProfile extends React.Component {
     static propTypes = {
         mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
-        text: PropTypes.string, // Text to show. Defaults to sender name
         onClick: PropTypes.func,
     };
 
@@ -118,17 +117,10 @@ export default class SenderProfile extends React.Component {
             { flair }
         </span>;
 
-        const content = this.props.text ?
-            <span>
-                <span className="mx_SenderProfile_aux">
-                    { _t(this.props.text, { senderName: () => nameElem }) }
-                </span>
-            </span> : nameFlair;
-
         return (
             <div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
                 <div className="mx_SenderProfile_hover">
-                    { content }
+                    { nameFlair }
                 </div>
             </div>
         );
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 87fb190678..2009eb6d1c 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -768,15 +768,10 @@ export default class EventTile extends React.Component {
         }
 
         if (needsSenderProfile) {
-            let text = null;
             if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') {
-                if (msgtype === 'm.image') text = _td('%(senderName)s sent an image');
-                else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video');
-                else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
                 sender = <SenderProfile onClick={this.onSenderProfileClick}
                                         mxEvent={this.props.mxEvent}
-                                        enableFlair={this.props.enableFlair && !text}
-                                        text={text} />;
+                                        enableFlair={this.props.enableFlair} />;
             } else {
                 sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={this.props.enableFlair} />;
             }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a1999acb3b..f3232416b1 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1430,9 +1430,6 @@
     "Edit message": "Edit message",
     "Mod": "Mod",
     "This event could not be displayed": "This event could not be displayed",
-    "%(senderName)s sent an image": "%(senderName)s sent an image",
-    "%(senderName)s sent a video": "%(senderName)s sent a video",
-    "%(senderName)s uploaded a file": "%(senderName)s uploaded a file",
     "Your key share request has been sent - please check your other sessions for key share requests.": "Your key share request has been sent - please check your other sessions for key share requests.",
     "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.": "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.",
     "If your other sessions do not have the key for this message you will not be able to decrypt them.": "If your other sessions do not have the key for this message you will not be able to decrypt them.",

From 8d143331a8d8fa72c89b8730f70b9b3ff9ba7009 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 4 Mar 2021 20:10:47 -0700
Subject: [PATCH 279/389] Appease the linter

---
 src/components/views/messages/SenderProfile.js | 1 -
 src/components/views/rooms/EventTile.js        | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js
index e2bb1ff38d..d2db05252c 100644
--- a/src/components/views/messages/SenderProfile.js
+++ b/src/components/views/messages/SenderProfile.js
@@ -18,7 +18,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import Flair from '../elements/Flair.js';
 import FlairStore from '../../../stores/FlairStore';
-import { _t } from '../../../languageHandler';
 import {getUserNameColorClass} from '../../../utils/FormattingUtils';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index 2009eb6d1c..198b3427bc 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -22,7 +22,7 @@ import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
 import classNames from "classnames";
 import {EventType} from "matrix-js-sdk/src/@types/event";
-import { _t, _td } from '../../../languageHandler';
+import { _t } from '../../../languageHandler';
 import * as TextForEvent from "../../../TextForEvent";
 import * as sdk from "../../../index";
 import dis from '../../../dispatcher/dispatcher';

From d7310bc5b36cb66abbee507ff74cea3f56592c70 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 4 Mar 2021 20:17:29 -0700
Subject: [PATCH 280/389] Remove dead classes

---
 res/css/views/rooms/_IRCLayout.scss | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss
index 792c2f1f58..21baa795e6 100644
--- a/res/css/views/rooms/_IRCLayout.scss
+++ b/res/css/views/rooms/_IRCLayout.scss
@@ -181,8 +181,7 @@ $irc-line-height: $font-18px;
         > span {
             display: flex;
 
-            > .mx_SenderProfile_name,
-            > .mx_SenderProfile_aux {
+            > .mx_SenderProfile_name {
                 overflow: hidden;
                 text-overflow: ellipsis;
                 min-width: var(--name-width);
@@ -212,8 +211,7 @@ $irc-line-height: $font-18px;
             background: transparent;
 
             > span {
-                > .mx_SenderProfile_name,
-                > .mx_SenderProfile_aux {
+                > .mx_SenderProfile_name {
                     min-width: inherit;
                 }
             }

From c80cbc38dd28e89d1b643189acb9c659914989dc Mon Sep 17 00:00:00 2001
From: David Baker <dbkr@users.noreply.github.com>
Date: Fri, 5 Mar 2021 10:32:54 +0000
Subject: [PATCH 281/389] Use helper class

(It did not need imports)

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

diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx
index 0503df038a..03e63edbcc 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> = ({
     const canModify = userWidget || WidgetUtils.canUserModifyWidgets(roomId);
 
     let streamAudioStreamButton;
-    if (getConfigLivestreamUrl() && (app.type === "m.jitsi" || app.type === "jitsi")) {
+    if (getConfigLivestreamUrl() && WidgetType.JITSI.matches(app.type)) {
         const onStreamAudioClick = async () => {
             try {
                 await startJitsiAudioLivestream(widgetMessaging, roomId);
@@ -199,4 +199,3 @@ const WidgetContextMenu: React.FC<IProps> = ({
 };
 
 export default WidgetContextMenu;
-

From 8bcf0f08385368ce0dce69510e814f478aa90e2c Mon Sep 17 00:00:00 2001
From: David Baker <dbkr@users.noreply.github.com>
Date: Fri, 5 Mar 2021 10:34:03 +0000
Subject: [PATCH 282/389] console.error

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

diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx
index 03e63edbcc..623fe04f2f 100644
--- a/src/components/views/context_menus/WidgetContextMenu.tsx
+++ b/src/components/views/context_menus/WidgetContextMenu.tsx
@@ -62,7 +62,7 @@ const WidgetContextMenu: React.FC<IProps> = ({
             try {
                 await startJitsiAudioLivestream(widgetMessaging, roomId);
             } catch (err) {
-                console.log("Failed to start livestream", err);
+                console.error("Failed to start livestream", err);
                 // XXX: won't i18n well, but looks like widget api only support 'message'?
                 const message = err.message || _t("Unable to start audio streaming.");
                 Modal.createTrackedDialog('WidgetContext Menu', 'Livestream failed', ErrorDialog, {

From 572f15522f28320f671980a5892ec2cf10fd0d8e Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 5 Mar 2021 10:40:11 +0000
Subject: [PATCH 283/389] use a constant

---
 src/Livestream.ts | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/Livestream.ts b/src/Livestream.ts
index cd8cdea179..2389132762 100644
--- a/src/Livestream.ts
+++ b/src/Livestream.ts
@@ -23,6 +23,9 @@ export function getConfigLivestreamUrl() {
     return SdkConfig.get()["audioStreamUrl"];
 }
 
+// Dummy rtmp URL used to signal that we want a special audio-only stream
+const AUDIOSTREAM_DUMMY_URL = 'rtmp://audiostream.dummy/';
+
 async function createLiveStream(roomId: string) {
     const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
 
@@ -47,6 +50,6 @@ export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi
     const streamId = await createLiveStream(roomId);
 
     await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, {
-        rtmpStreamKey: 'rtmp://audiostream.dummy/' + streamId,
+        rtmpStreamKey: AUDIOSTREAM_DUMMY_URL + streamId,
     });
 }

From 1c1d239d5b0853e2b6ee56f823020655d2a68a2b Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Fri, 5 Mar 2021 13:19:06 +0000
Subject: [PATCH 284/389] Add Edge to the targets list

Part of https://github.com/vector-im/element-web/issues/9175
---
 babel.config.js | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/babel.config.js b/babel.config.js
index d5a97d56ce..0a3a34a391 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -3,12 +3,15 @@ module.exports = {
     "presets": [
         ["@babel/preset-env", {
             "targets": [
-                "last 2 Chrome versions", "last 2 Firefox versions", "last 2 Safari versions"
+                "last 2 Chrome versions",
+                "last 2 Firefox versions",
+                "last 2 Safari versions",
+                "last 2 Edge versions",
             ],
         }],
         "@babel/preset-typescript",
         "@babel/preset-flow",
-        "@babel/preset-react"
+        "@babel/preset-react",
     ],
     "plugins": [
         ["@babel/plugin-proposal-decorators", {legacy: true}],
@@ -18,6 +21,6 @@ module.exports = {
         "@babel/plugin-proposal-object-rest-spread",
         "@babel/plugin-transform-flow-comments",
         "@babel/plugin-syntax-dynamic-import",
-        "@babel/plugin-transform-runtime"
-    ]
+        "@babel/plugin-transform-runtime",
+    ],
 };

From 35dbc82b87f5aba8b5ab91afa4a7afb6d0ab5a4a Mon Sep 17 00:00:00 2001
From: Will Hunt <will@half-shot.uk>
Date: Fri, 5 Mar 2021 15:01:48 +0000
Subject: [PATCH 285/389] Add issue comments

---
 src/components/structures/LoggedInView.tsx | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 508b7f05e7..5acc8c6891 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -93,6 +93,8 @@ interface IProps {
 }
 
 interface IUsageLimit {
+    // "hs_disabled" is NOT a specced string, but is used in Synapse
+    // This is tracked over at https://github.com/matrix-org/synapse/issues/9237
     // eslint-disable-next-line camelcase
     limit_type: "monthly_active_user" | "hs_disabled" | string;
     // eslint-disable-next-line camelcase
@@ -102,6 +104,8 @@ interface IUsageLimit {
 interface IState {
     syncErrorData?: {
         error: {
+            // This is not specced, but used in Synapse. See
+            // https://github.com/matrix-org/synapse/issues/9237#issuecomment-768238922
             data: IUsageLimit;
             errcode: string;
         };

From e5b03488d890a3c289cd06d0e2ab8dd11bbb77ce Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Fri, 5 Mar 2021 17:52:51 +0000
Subject: [PATCH 286/389] Fix widget resizing

There was a line of CSS that set pointer-events: none on widget
iframes whilst they were being resized to stop iframes swallowing
the mousemove/up events while dragging the resize handle, but a)
all widgets are now in a persisted element wrapper and therefore
not in the right place in the DOM to get that CSS and b) that only
got set when resizing the whole aps drawer vertically, not dragging
the handle between apps to change the width distribution.

Add a pointer events prop to AppTile to allow the pointer-events
style to be set by the parent, and set it when dragging either
resize handle.

Fixes https://github.com/vector-im/element-web/issues/16473
---
 res/css/views/rooms/_AppsDrawer.scss     |  5 -----
 src/components/views/elements/AppTile.js | 14 ++++++++++----
 src/components/views/rooms/AppsDrawer.js | 14 +++++++++++++-
 3 files changed, 23 insertions(+), 10 deletions(-)

diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss
index 492ed95973..fd80836237 100644
--- a/res/css/views/rooms/_AppsDrawer.scss
+++ b/res/css/views/rooms/_AppsDrawer.scss
@@ -370,11 +370,6 @@ $MinWidth: 240px;
     display: none;
 }
 
-/* Avoid apptile iframes capturing mouse event focus when resizing */
-.mx_AppsDrawer_resizing iframe {
-    pointer-events: none;
-}
-
 .mx_AppsDrawer_resizing .mx_AppTile_persistedWrapper {
     z-index: 1;
 }
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 213351889f..747d00a8df 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -328,6 +328,10 @@ export default class AppTile extends React.Component {
         const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture;";
 
         const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini  ' : ' ');
+        const appTileBodyStyles = {};
+        if (this.props.pointerEvents) {
+            appTileBodyStyles['pointer-events'] = this.props.pointerEvents;
+        }
 
         const loadingElement = (
             <div className="mx_AppLoading_spinner_fadeIn">
@@ -338,7 +342,7 @@ export default class AppTile extends React.Component {
             // only possible for room widgets, can assert this.props.room here
             const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
             appTileBody = (
-                <div className={appTileBodyClass}>
+                <div className={appTileBodyClass} style={appTileBodyStyles}>
                     <AppPermission
                         roomId={this.props.room.roomId}
                         creatorUserId={this.props.creatorUserId}
@@ -350,20 +354,20 @@ export default class AppTile extends React.Component {
             );
         } else if (this.state.initialising) {
             appTileBody = (
-                <div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
+                <div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')} style={appTileBodyStyles}>
                     { loadingElement }
                 </div>
             );
         } else {
             if (this.isMixedContent()) {
                 appTileBody = (
-                    <div className={appTileBodyClass}>
+                    <div className={appTileBodyClass} style={appTileBodyStyles}>
                         <AppWarning errorMsg="Error - Mixed content" />
                     </div>
                 );
             } else {
                 appTileBody = (
-                    <div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
+                    <div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')} style={appTileBodyStyles}>
                         { this.state.loading && loadingElement }
                         <iframe
                             allow={iframeFeatures}
@@ -477,6 +481,8 @@ AppTile.propTypes = {
     showPopout: PropTypes.bool,
     // Is this an instance of a user widget
     userWidget: PropTypes.bool,
+    // sets the pointer-events property on the iframe
+    pointerEvents: PropTypes.string,
 };
 
 AppTile.defaultProps = {
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index ef30e4a8f5..aa7120bbe6 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -53,6 +53,8 @@ export default class AppsDrawer extends React.Component {
 
         this.state = {
             apps: this._getApps(),
+            resizingVertical: false, // true when changing the height of the apps drawer
+            resizingHorizontal: false, // true when chagning the distribution of the width between widgets
         };
 
         this._resizeContainer = null;
@@ -85,13 +87,16 @@ export default class AppsDrawer extends React.Component {
     }
 
     onIsResizing = (resizing) => {
-        this.setState({ resizing });
+        // This one is the vertical, ie. change height of apps drawer
+        this.setState({ resizingVertical: resizing });
         if (!resizing) {
             this._relaxResizer();
         }
     };
 
     _createResizer() {
+        // This is the horizontal one, changing the distribution of the width between the app tiles
+        // (ie. a vertical resize handle because, the handle itself is vertical...)
         const classNames = {
             handle: "mx_ResizeHandle",
             vertical: "mx_ResizeHandle_vertical",
@@ -100,6 +105,7 @@ export default class AppsDrawer extends React.Component {
         const collapseConfig = {
             onResizeStart: () => {
                 this._resizeContainer.classList.add("mx_AppsDrawer_resizing");
+                this.setState({ resizingHorizontal: true });
             },
             onResizeStop: () => {
                 this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
@@ -107,6 +113,7 @@ export default class AppsDrawer extends React.Component {
                     this.props.room, Container.Top,
                     this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
                 );
+                this.setState({ resizingHorizontal: false });
             },
         };
         // pass a truthy container for now, we won't call attach until we update it
@@ -162,6 +169,10 @@ export default class AppsDrawer extends React.Component {
         }
     };
 
+    isResizing() {
+        return this.state.resizingVertical || this.state.resizingHorizontal;
+    }
+
     onAction = (action) => {
         const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
         switch (action.action) {
@@ -209,6 +220,7 @@ export default class AppsDrawer extends React.Component {
                 creatorUserId={app.creatorUserId}
                 widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
                 waitForIframeLoad={app.waitForIframeLoad}
+                pointerEvents={this.isResizing() ? 'none' : undefined}
             />);
         });
 

From ca63e937d396387254c91d225735b288cc2c6701 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 10:56:13 -0700
Subject: [PATCH 287/389] Fix up logic to show sent receipts correctly

---
 src/components/structures/MessagePanel.js | 10 ++++++++
 src/components/views/rooms/EventTile.js   | 29 +++++++++++++----------
 2 files changed, 27 insertions(+), 12 deletions(-)

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 161227a139..dab1c953a9 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -595,6 +595,15 @@ export default class MessagePanel extends React.Component {
 
         const readReceipts = this._readReceiptsByEvent[eventId];
 
+        let isLastSuccessful = false;
+        const isSentState = s => !s || s === 'sent';
+        const isSent = isSentState(mxEv.getAssociatedStatus())
+        if (!nextEvent && isSent) {
+            isLastSuccessful = true;
+        } else if (nextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
+            isLastSuccessful = true;
+        }
+
         // use txnId as key if available so that we don't remount during sending
         ret.push(
             <li
@@ -620,6 +629,7 @@ export default class MessagePanel extends React.Component {
                         permalinkCreator={this.props.permalinkCreator}
                         last={last}
                         lastInSection={willWantDateSeparator}
+                        lastSuccessful={isLastSuccessful}
                         isSelectedEvent={highlight}
                         getRelationsForEvent={this.props.getRelationsForEvent}
                         showReactions={this.props.showReactions}
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index b4192fc8d3..52010c8c2e 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -171,6 +171,9 @@ export default class EventTile extends React.Component {
         // targeting)
         lastInSection: PropTypes.bool,
 
+        // True if the event is the last successful (sent) event.
+        isLastSuccessful: PropTypes.bool,
+
         /* true if this is search context (which has the effect of greying out
          * the text
          */
@@ -297,7 +300,11 @@ export default class EventTile extends React.Component {
         // events and pretty much anything that can't be sent by the composer as a message. For
         // those we rely on local echo giving the impression of things changing, and expect them
         // to be quick.
-        const simpleSendableEvents = [EventType.Sticker, EventType.RoomMessage, EventType.RoomMessageEncrypted];
+        const simpleSendableEvents = [
+            EventType.Sticker,
+            EventType.RoomMessage,
+            EventType.RoomMessageEncrypted,
+        ];
         if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false;
 
         // Default case
@@ -308,22 +315,20 @@ export default class EventTile extends React.Component {
         // If we're not even eligible, don't show the receipt.
         if (!this._isEligibleForSpecialReceipt) return false;
 
+        // We only show the 'sent' receipt on the last successful event.
+        if (!this.props.lastSuccessful) return false;
+
         // Check to make sure the sending state is appropriate. A null/undefined send status means
         // that the message is 'sent', so we're just double checking that it's explicitly not sent.
         if (this.props.eventSendStatus && this.props.eventSendStatus !== 'sent') return false;
 
-        // No point in doing the complex math if we're not going to even show this special receipt.
-        if (this._shouldShowSendingReceipt) return false;
-
-        // Next we check to see if any newer events have read receipts. If they do then we don't
-        // show our special state - the user already has feedback about their message. We only
-        // search for the most recent 50 events because surely someone will have sent *something*
-        // in that time, even if it is a membership event or something.
-        const room = this.context.getRoom(this.props.mxEvent.getRoomId());
+        // If anyone has read the event besides us, we don't want to show a sent receipt.
+        const receipts = this.props.readReceipts || [];
         const myUserId = MatrixClientPeg.get().getUserId();
-        const readUsers = room.getUsersWhoHaveRead(this.props.mxEvent, 50);
-        const hasBeenRead = readUsers.length === 0 || readUsers.some(u => u !== myUserId);
-        return !hasBeenRead;
+        if (receipts.some(r => r.userId !== myUserId)) return false;
+
+        // Finally, we should show a receipt.
+        return true;
     }
 
     get _shouldShowSendingReceipt() {

From 2c64dac5140004e41623d3337d163a9ed1d0babb Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Fri, 5 Mar 2021 19:57:37 +0200
Subject: [PATCH 288/389] fix: show decrypted source on EditHistory ->
 ViewSource

pass correct props to the component
---
 src/components/views/messages/EditHistoryMessage.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
index df27773a40..68a3c95745 100644
--- a/src/components/views/messages/EditHistoryMessage.js
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -77,6 +77,8 @@ export default class EditHistoryMessage extends React.PureComponent {
             roomId: this.props.mxEvent.getRoomId(),
             eventId: this.props.mxEvent.getId(),
             content: this.props.mxEvent.event,
+            isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(),
+            decryptedContent: this.props.mxEvent._clearEvent,
         }, 'mx_Dialog_viewsource');
     };
 

From f87f2b11ef945d7d78e8912e6df9b75514f10764 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 11:05:56 -0700
Subject: [PATCH 289/389] Appease the linter

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

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index dab1c953a9..c1ccf61470 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -597,7 +597,7 @@ export default class MessagePanel extends React.Component {
 
         let isLastSuccessful = false;
         const isSentState = s => !s || s === 'sent';
-        const isSent = isSentState(mxEv.getAssociatedStatus())
+        const isSent = isSentState(mxEv.getAssociatedStatus());
         if (!nextEvent && isSent) {
             isLastSuccessful = true;
         } else if (nextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {

From ae9618367eef5a4c6a429f101ee5f1b424c50d1d Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 13:06:53 -0700
Subject: [PATCH 290/389] Convert UploadBar to TypeScript

---
 .../{UploadBar.js => UploadBar.tsx}           | 21 ++++++++++++-------
 1 file changed, 13 insertions(+), 8 deletions(-)
 rename src/components/structures/{UploadBar.js => UploadBar.tsx} (90%)

diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.tsx
similarity index 90%
rename from src/components/structures/UploadBar.js
rename to src/components/structures/UploadBar.tsx
index 16cc4cb987..27d6746698 100644
--- a/src/components/structures/UploadBar.js
+++ b/src/components/structures/UploadBar.tsx
@@ -1,6 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2015, 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,16 +15,22 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import ContentMessages from '../../ContentMessages';
 import dis from "../../dispatcher/dispatcher";
 import filesize from "filesize";
 import { _t } from '../../languageHandler';
+import {Room} from "matrix-js-sdk/src/models/room";
 
-export default class UploadBar extends React.Component {
-    static propTypes = {
-        room: PropTypes.object,
-    };
+interface IProps {
+    room: Room;
+}
+
+interface IState {
+}
+
+export default class UploadBar extends React.Component<IProps, IState> {
+    private dispatcherRef: string;
+    private mounted: boolean;
 
     componentDidMount() {
         this.dispatcherRef = dis.register(this.onAction);
@@ -37,7 +42,7 @@ export default class UploadBar extends React.Component {
         dis.unregister(this.dispatcherRef);
     }
 
-    onAction = payload => {
+    private onAction = (payload) => {
         switch (payload.action) {
             case 'upload_progress':
             case 'upload_finished':

From bb80cfb9a66fd26af9571b8be3c0979d3cb944a6 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 13:20:50 -0700
Subject: [PATCH 291/389] Convert all of file uploads to the new dispatcher

---
 src/ContentMessages.tsx                  | 27 ++++++-------
 src/components/structures/RoomView.tsx   |  6 +--
 src/components/structures/UploadBar.tsx  | 12 +++---
 src/dispatcher/actions.ts                | 25 ++++++++++++
 src/dispatcher/payloads/UploadPayload.ts | 51 ++++++++++++++++++++++++
 src/models/IUpload.ts                    | 24 +++++++++++
 6 files changed, 123 insertions(+), 22 deletions(-)
 create mode 100644 src/dispatcher/payloads/UploadPayload.ts
 create mode 100644 src/models/IUpload.ts

diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx
index bec36d49f6..95b45cce4a 100644
--- a/src/ContentMessages.tsx
+++ b/src/ContentMessages.tsx
@@ -32,6 +32,14 @@ import Spinner from "./components/views/elements/Spinner";
 import "blueimp-canvas-to-blob";
 import { Action } from "./dispatcher/actions";
 import CountlyAnalytics from "./CountlyAnalytics";
+import {
+    UploadCanceledPayload,
+    UploadErrorPayload,
+    UploadFinishedPayload,
+    UploadProgressPayload,
+    UploadStartedPayload,
+} from "./dispatcher/payloads/UploadPayload";
+import {IUpload} from "./models/IUpload";
 
 const MAX_WIDTH = 800;
 const MAX_HEIGHT = 600;
@@ -44,15 +52,6 @@ export class UploadCanceledError extends Error {}
 
 type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
 
-interface IUpload {
-    fileName: string;
-    roomId: string;
-    total: number;
-    loaded: number;
-    promise: Promise<any>;
-    canceled?: boolean;
-}
-
 interface IMediaConfig {
     "m.upload.size"?: number;
 }
@@ -478,7 +477,7 @@ export default class ContentMessages {
         if (upload) {
             upload.canceled = true;
             MatrixClientPeg.get().cancelUpload(upload.promise);
-            dis.dispatch({action: 'upload_canceled', upload});
+            dis.dispatch<UploadCanceledPayload>({action: Action.UploadCanceled, upload});
         }
     }
 
@@ -539,7 +538,7 @@ export default class ContentMessages {
             promise: prom,
         };
         this.inprogress.push(upload);
-        dis.dispatch({action: 'upload_started'});
+        dis.dispatch<UploadStartedPayload>({action: Action.UploadStarted, upload});
 
         // Focus the composer view
         dis.fire(Action.FocusComposer);
@@ -547,7 +546,7 @@ export default class ContentMessages {
         function onProgress(ev) {
             upload.total = ev.total;
             upload.loaded = ev.loaded;
-            dis.dispatch({action: 'upload_progress', upload: upload});
+            dis.dispatch<UploadProgressPayload>({action: Action.UploadProgress, upload});
         }
 
         let error;
@@ -601,9 +600,9 @@ export default class ContentMessages {
                 if (error && error.http_status === 413) {
                     this.mediaConfig = null;
                 }
-                dis.dispatch({action: 'upload_failed', upload, error});
+                dis.dispatch<UploadErrorPayload>({action: Action.UploadFailed, upload, error});
             } else {
-                dis.dispatch({action: 'upload_finished', upload});
+                dis.dispatch<UploadFinishedPayload>({action: Action.UploadFinished, upload});
                 dis.dispatch({action: 'message_sent'});
             }
         });
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 96808e651e..90f6daf6cb 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -711,9 +711,9 @@ export default class RoomView extends React.Component<IProps, IState> {
                     [payload.file], this.state.room.roomId, this.context);
                 break;
             case 'notifier_enabled':
-            case 'upload_started':
-            case 'upload_finished':
-            case 'upload_canceled':
+            case Action.UploadStarted:
+            case Action.UploadFinished:
+            case Action.UploadCanceled:
                 this.forceUpdate();
                 break;
             case 'call_state': {
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index 27d6746698..f60e28b333 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -20,6 +20,8 @@ import dis from "../../dispatcher/dispatcher";
 import filesize from "filesize";
 import { _t } from '../../languageHandler';
 import {Room} from "matrix-js-sdk/src/models/room";
+import {ActionPayload} from "../../dispatcher/payloads";
+import {Action} from "../../dispatcher/actions";
 
 interface IProps {
     room: Room;
@@ -42,12 +44,12 @@ export default class UploadBar extends React.Component<IProps, IState> {
         dis.unregister(this.dispatcherRef);
     }
 
-    private onAction = (payload) => {
+    private onAction = (payload: ActionPayload) => {
         switch (payload.action) {
-            case 'upload_progress':
-            case 'upload_finished':
-            case 'upload_canceled':
-            case 'upload_failed':
+            case Action.UploadProgress:
+            case Action.UploadFinished:
+            case Action.UploadCanceled:
+            case Action.UploadFailed:
                 if (this.mounted) this.forceUpdate();
                 break;
         }
diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts
index 12bf4c57a3..cd32c3743f 100644
--- a/src/dispatcher/actions.ts
+++ b/src/dispatcher/actions.ts
@@ -113,4 +113,29 @@ export enum Action {
      * XXX: Ditto
      */
     VirtualRoomSupportUpdated = "virtual_room_support_updated",
+
+    /**
+     * Fired when an upload has started. Should be used with UploadStartedPayload.
+     */
+    UploadStarted = "upload_started",
+
+    /**
+     * Fired when an upload makes progress. Should be used with UploadProgressPayload.
+     */
+    UploadProgress = "upload_progress",
+
+    /**
+     * Fired when an upload is completed. Should be used with UploadFinishedPayload.
+     */
+    UploadFinished = "upload_finished",
+
+    /**
+     * Fired when an upload fails. Should be used with UploadErrorPayload.
+     */
+    UploadFailed = "upload_failed",
+
+    /**
+     * Fired when an upload is cancelled by the user. Should be used with UploadCanceledPayload.
+     */
+    UploadCanceled = "upload_canceled",
 }
diff --git a/src/dispatcher/payloads/UploadPayload.ts b/src/dispatcher/payloads/UploadPayload.ts
new file mode 100644
index 0000000000..40c2710dd6
--- /dev/null
+++ b/src/dispatcher/payloads/UploadPayload.ts
@@ -0,0 +1,51 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { ActionPayload } from "../payloads";
+import { Action } from "../actions";
+import {IUpload} from "../../models/IUpload";
+
+interface UploadPayload extends ActionPayload {
+    /**
+     * The upload with fields representing the new upload state.
+     */
+    upload: IUpload;
+}
+
+export interface UploadStartedPayload extends UploadPayload {
+    action: Action.UploadStarted;
+}
+
+export interface UploadProgressPayload extends UploadPayload {
+    action: Action.UploadProgress;
+}
+
+export interface UploadErrorPayload extends UploadPayload {
+    action: Action.UploadFailed;
+
+    /**
+     * An error to describe what went wrong with the upload.
+     */
+    error: Error;
+}
+
+export interface UploadFinishedPayload extends UploadPayload {
+    action: Action.UploadFinished;
+}
+
+export interface UploadCanceledPayload extends UploadPayload {
+    action: Action.UploadCanceled;
+}
diff --git a/src/models/IUpload.ts b/src/models/IUpload.ts
new file mode 100644
index 0000000000..5b376e9330
--- /dev/null
+++ b/src/models/IUpload.ts
@@ -0,0 +1,24 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+export interface IUpload {
+    fileName: string;
+    roomId: string;
+    total: number;
+    loaded: number;
+    promise: Promise<any>;
+    canceled?: boolean;
+}

From 711181cc695dabca5c46578a514b6dae97dd55da Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 14:14:43 -0700
Subject: [PATCH 292/389] Rough style for new upload bar

This repurposes ProgressBar which was not used anywhere in code.
---
 res/css/_common.scss                          |  7 +++
 res/css/structures/_UploadBar.scss            | 26 +++--------
 res/css/views/elements/_ProgressBar.scss      | 13 +++---
 res/themes/dark/css/_dark.scss                |  3 ++
 res/themes/legacy-dark/css/_legacy-dark.scss  |  3 ++
 .../legacy-light/css/_legacy-light.scss       |  3 +-
 res/themes/light/css/_light.scss              |  3 +-
 src/components/structures/UploadBar.tsx       | 44 ++++++-------------
 8 files changed, 42 insertions(+), 60 deletions(-)

diff --git a/res/css/_common.scss b/res/css/_common.scss
index 6e9d252659..8ae1cc6641 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -606,6 +606,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
     }
 }
 
+@define-mixin ProgressBarBgColour $colour {
+    background-color: $colour;
+    &::-webkit-progress-bar {
+        background-color: $colour;
+    }
+}
+
 @define-mixin ProgressBarBorderRadius $radius {
     border-radius: $radius;
     &::-moz-progress-bar {
diff --git a/res/css/structures/_UploadBar.scss b/res/css/structures/_UploadBar.scss
index d76c81668c..ce884e27a9 100644
--- a/res/css/structures/_UploadBar.scss
+++ b/res/css/structures/_UploadBar.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015, 2016, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,24 +15,15 @@ limitations under the License.
 */
 
 .mx_UploadBar {
-    position: relative;
-}
+    padding-left: 65px; // line up with the shield area in the composer
 
-.mx_UploadBar_uploadProgressOuter {
-    height: 5px;
-    margin-left: 63px;
-    margin-top: -1px;
-    padding-bottom: 5px;
-}
-
-.mx_UploadBar_uploadProgressInner {
-    background-color: $accent-color;
-    height: 5px;
+    .mx_ProgressBar {
+        width: calc(100% - 40px); // cheating at a right margin
+    }
 }
 
 .mx_UploadBar_uploadFilename {
     margin-top: 5px;
-    margin-left: 65px;
     opacity: 0.5;
     color: $primary-fg-color;
 }
@@ -52,10 +43,3 @@ limitations under the License.
     cursor: pointer;
     z-index: 1;
 }
-
-.mx_UploadBar_uploadBytes {
-    float: right;
-    margin-top: 5px;
-    margin-right: 30px;
-    color: $accent-color;
-}
diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss
index e49d85af04..5598ddba20 100644
--- a/res/css/views/elements/_ProgressBar.scss
+++ b/res/css/views/elements/_ProgressBar.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,15 +15,16 @@ limitations under the License.
 */
 
 progress.mx_ProgressBar {
-    height: 4px;
+    height: 6px;
     width: 60px;
-    border-radius: 10px;
+    border-radius: 6px;
     overflow: hidden;
     appearance: none;
-    border: 0;
+    border: none;
 
-    @mixin ProgressBarBorderRadius "10px";
-    @mixin ProgressBarColour $accent-color;
+    @mixin ProgressBarBorderRadius "6px";
+    @mixin ProgressBarColour $progressbar-fg-color;
+    @mixin ProgressBarBgColour $progressbar-bg-color;
     ::-webkit-progress-value {
         transition: width 1s;
     }
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 0de5e69782..4b8c17fc78 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -173,6 +173,9 @@ $button-link-bg-color: transparent;
 // Toggle switch
 $togglesw-off-color: $room-highlight-color;
 
+$progressbar-fg-color: $accent-color;
+$progressbar-bg-color: #21262c;
+
 $visual-bell-bg-color: #800;
 
 $room-warning-bg-color: $header-panel-bg-color;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 8c5f20178b..892f63a2c7 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -168,6 +168,9 @@ $button-link-bg-color: transparent;
 // Toggle switch
 $togglesw-off-color: $room-highlight-color;
 
+$progressbar-fg-color: $accent-color;
+$progressbar-bg-color: #21262c;
+
 $visual-bell-bg-color: #800;
 
 $room-warning-bg-color: $header-panel-bg-color;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 3ba10a68ea..28e8ed9ea1 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -282,7 +282,8 @@ $togglesw-ball-color: #fff;
 $slider-selection-color: $accent-color;
 $slider-background-color: #c1c9d6;
 
-$progressbar-color: #000;
+$progressbar-fg-color: $accent-color;
+$progressbar-bg-color: rgba(141, 151, 165, 0.2);
 
 $room-warning-bg-color: $yellow-background;
 
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index b6906d16be..b118062e30 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -279,7 +279,8 @@ $togglesw-ball-color: #fff;
 $slider-selection-color: $accent-color;
 $slider-background-color: #c1c9d6;
 
-$progressbar-color: #000;
+$progressbar-fg-color: $accent-color;
+$progressbar-bg-color: rgba(141, 151, 165, 0.2);
 
 $room-warning-bg-color: $yellow-background;
 
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index f60e28b333..3933ef0f9e 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -22,6 +22,7 @@ import { _t } from '../../languageHandler';
 import {Room} from "matrix-js-sdk/src/models/room";
 import {ActionPayload} from "../../dispatcher/payloads";
 import {Action} from "../../dispatcher/actions";
+import ProgressBar from "../views/elements/ProgressBar";
 
 interface IProps {
     room: Room;
@@ -68,48 +69,29 @@ export default class UploadBar extends React.Component<IProps, IState> {
         //     fileName: "testing_fooble.jpg",
         // }];
 
-        if (uploads.length == 0) {
-            return <div />;
+        const uploadsHere = uploads.filter(u => u.roomId === this.props.room.roomId);
+        if (uploadsHere.length == 0) {
+            return null;
         }
 
-        let upload;
-        for (let i = 0; i < uploads.length; ++i) {
-            if (uploads[i].roomId == this.props.room.roomId) {
-                upload = uploads[i];
-                break;
-            }
-        }
-        if (!upload) {
-            return <div />;
-        }
-
-        const innerProgressStyle = {
-            width: ((upload.loaded / (upload.total || 1)) * 100) + '%',
-        };
-        let uploadedSize = filesize(upload.loaded);
-        const totalSize = filesize(upload.total);
-        if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
-            uploadedSize = uploadedSize.replace(/ .*/, '');
-        }
+        const currentUpload = uploadsHere[0];
+        const uploadSize = filesize(currentUpload.total);
 
         // MUST use var name 'count' for pluralization to kick in
         const uploadText = _t(
-            "Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)},
+            "Uploading %(filename)s and %(count)s others", {
+                filename: currentUpload.fileName,
+                count: uploadsHere.length - 1,
+            },
         );
 
         return (
             <div className="mx_UploadBar">
-                <div className="mx_UploadBar_uploadProgressOuter">
-                    <div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
-                </div>
-                <img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src={require("../../../res/img/fileicon.png")} width="17" height="22" />
                 <img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src={require("../../../res/img/cancel.svg")} width="18" height="18"
-                    onClick={function() { ContentMessages.sharedInstance().cancelUpload(upload.promise); }}
+                    onClick={function() { ContentMessages.sharedInstance().cancelUpload(currentUpload.promise); }}
                 />
-                <div className="mx_UploadBar_uploadBytes">
-                    { uploadedSize } / { totalSize }
-                </div>
-                <div className="mx_UploadBar_uploadFilename">{ uploadText }</div>
+                <div className="mx_UploadBar_uploadFilename">{uploadText} ({uploadSize})</div>
+                <ProgressBar value={currentUpload.loaded} max={currentUpload.total} />
             </div>
         );
     }

From fa41489d5ae343c999f8788cce495107db94a394 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 14:24:02 -0700
Subject: [PATCH 293/389] Refactor UploadBar into component state

---
 src/components/structures/UploadBar.tsx | 54 +++++++++++++------------
 1 file changed, 28 insertions(+), 26 deletions(-)

diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index 3933ef0f9e..509b8c8363 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -23,18 +23,27 @@ import {Room} from "matrix-js-sdk/src/models/room";
 import {ActionPayload} from "../../dispatcher/payloads";
 import {Action} from "../../dispatcher/actions";
 import ProgressBar from "../views/elements/ProgressBar";
+import AccessibleButton from "../views/elements/AccessibleButton";
+import {IUpload} from "../../models/IUpload";
 
 interface IProps {
     room: Room;
 }
 
 interface IState {
+    currentUpload?: IUpload;
+    uploadsHere: IUpload[];
 }
 
 export default class UploadBar extends React.Component<IProps, IState> {
     private dispatcherRef: string;
     private mounted: boolean;
 
+    constructor(props) {
+        super(props);
+        this.state = {uploadsHere: []};
+    }
+
     componentDidMount() {
         this.dispatcherRef = dis.register(this.onAction);
         this.mounted = true;
@@ -47,51 +56,44 @@ export default class UploadBar extends React.Component<IProps, IState> {
 
     private onAction = (payload: ActionPayload) => {
         switch (payload.action) {
+            case Action.UploadStarted:
             case Action.UploadProgress:
             case Action.UploadFinished:
             case Action.UploadCanceled:
-            case Action.UploadFailed:
-                if (this.mounted) this.forceUpdate();
+            case Action.UploadFailed: {
+                if (!this.mounted) return;
+                const uploads = ContentMessages.sharedInstance().getCurrentUploads();
+                const uploadsHere = uploads.filter(u => u.roomId === this.props.room.roomId);
+                this.setState({currentUpload: uploadsHere[0], uploadsHere});
                 break;
+            }
         }
     };
 
+    private onCancelClick = (ev) => {
+        ev.preventDefault();
+        ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise);
+    };
+
     render() {
-        const uploads = ContentMessages.sharedInstance().getCurrentUploads();
-
-        // for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
-        // check in RoomView
-        //
-        // uploads = [{
-        //     roomId: this.props.room.roomId,
-        //     loaded: 123493,
-        //     total: 347534,
-        //     fileName: "testing_fooble.jpg",
-        // }];
-
-        const uploadsHere = uploads.filter(u => u.roomId === this.props.room.roomId);
-        if (uploadsHere.length == 0) {
+        if (!this.state.currentUpload) {
             return null;
         }
 
-        const currentUpload = uploadsHere[0];
-        const uploadSize = filesize(currentUpload.total);
-
         // MUST use var name 'count' for pluralization to kick in
         const uploadText = _t(
             "Uploading %(filename)s and %(count)s others", {
-                filename: currentUpload.fileName,
-                count: uploadsHere.length - 1,
+                filename: this.state.currentUpload.fileName,
+                count: this.state.uploadsHere.length - 1,
             },
         );
-
+        
+        const uploadSize = filesize(this.state.currentUpload.total);
         return (
             <div className="mx_UploadBar">
-                <img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src={require("../../../res/img/cancel.svg")} width="18" height="18"
-                    onClick={function() { ContentMessages.sharedInstance().cancelUpload(currentUpload.promise); }}
-                />
+                <AccessibleButton onClick={this.onCancelClick} className='mx_UploadBar_cancel' />
                 <div className="mx_UploadBar_uploadFilename">{uploadText} ({uploadSize})</div>
-                <ProgressBar value={currentUpload.loaded} max={currentUpload.total} />
+                <ProgressBar value={this.state.currentUpload.loaded} max={this.state.currentUpload.total} />
             </div>
         );
     }

From 08072aca9aa35f0bcef03bcd4913299d9860157b Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 14:42:15 -0700
Subject: [PATCH 294/389] Add upload icon, fix cancel button, refresh styles

---
 res/css/structures/_UploadBar.scss      | 50 ++++++++++++++++---------
 res/img/element-icons/upload.svg        |  4 ++
 src/components/structures/UploadBar.tsx |  4 +-
 3 files changed, 38 insertions(+), 20 deletions(-)
 create mode 100644 res/img/element-icons/upload.svg

diff --git a/res/css/structures/_UploadBar.scss b/res/css/structures/_UploadBar.scss
index ce884e27a9..7c62516b47 100644
--- a/res/css/structures/_UploadBar.scss
+++ b/res/css/structures/_UploadBar.scss
@@ -16,30 +16,44 @@ limitations under the License.
 
 .mx_UploadBar {
     padding-left: 65px; // line up with the shield area in the composer
+    position: relative;
 
     .mx_ProgressBar {
         width: calc(100% - 40px); // cheating at a right margin
     }
 }
 
-.mx_UploadBar_uploadFilename {
+.mx_UploadBar_filename {
     margin-top: 5px;
-    opacity: 0.5;
-    color: $primary-fg-color;
-}
-
-.mx_UploadBar_uploadIcon {
-    float: left;
-    margin-top: 5px;
-    margin-left: 14px;
-}
-
-.mx_UploadBar_uploadCancel {
-    float: right;
-    margin-top: 5px;
-    margin-right: 10px;
+    color: $muted-fg-color;
     position: relative;
-    opacity: 0.6;
-    cursor: pointer;
-    z-index: 1;
+    padding-left: 22px; // 18px for icon, 4px for padding
+    font-size: $font-15px;
+    vertical-align: middle;
+
+    &::before {
+        content: "";
+        height: 18px;
+        width: 18px;
+        position: absolute;
+        top: 0;
+        left: 0;
+        mask-repeat: no-repeat;
+        mask-position: center;
+        background-color: $muted-fg-color;
+        mask-image: url('$(res)/img/element-icons/upload.svg');
+    }
+}
+
+.mx_UploadBar_cancel {
+    position: absolute;
+    top: 0;
+    right: 0;
+    height: 16px;
+    width: 16px;
+    margin-right: 16px; // align over rightmost button in composer
+    mask-repeat: no-repeat;
+    mask-position: center;
+    background-color: $muted-fg-color;
+    mask-image: url('$(res)/img/icons-close.svg');
 }
diff --git a/res/img/element-icons/upload.svg b/res/img/element-icons/upload.svg
new file mode 100644
index 0000000000..71ad7ba1cf
--- /dev/null
+++ b/res/img/element-icons/upload.svg
@@ -0,0 +1,4 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.99902 14L8.99902 4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M12.5352 7.52441L8.99944 4.00012L5.46373 7.52441" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index 509b8c8363..44780c566f 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -87,12 +87,12 @@ export default class UploadBar extends React.Component<IProps, IState> {
                 count: this.state.uploadsHere.length - 1,
             },
         );
-        
+
         const uploadSize = filesize(this.state.currentUpload.total);
         return (
             <div className="mx_UploadBar">
+                <div className="mx_UploadBar_filename">{uploadText} ({uploadSize})</div>
                 <AccessibleButton onClick={this.onCancelClick} className='mx_UploadBar_cancel' />
-                <div className="mx_UploadBar_uploadFilename">{uploadText} ({uploadSize})</div>
                 <ProgressBar value={this.state.currentUpload.loaded} max={this.state.currentUpload.total} />
             </div>
         );

From 757597e55fc6a1a4911caa780425689f818d988c Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 16:29:14 -0700
Subject: [PATCH 295/389] Fixed review concerns

---
 res/css/views/elements/_ProgressBar.scss | 1 -
 src/components/structures/UploadBar.tsx  | 8 ++++----
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss
index 5598ddba20..770978e921 100644
--- a/res/css/views/elements/_ProgressBar.scss
+++ b/res/css/views/elements/_ProgressBar.scss
@@ -17,7 +17,6 @@ limitations under the License.
 progress.mx_ProgressBar {
     height: 6px;
     width: 60px;
-    border-radius: 6px;
     overflow: hidden;
     appearance: none;
     border: none;
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index 44780c566f..b9d157ee00 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -15,16 +15,16 @@ limitations under the License.
 */
 
 import React from 'react';
+import { Room } from "matrix-js-sdk/src/models/room";
 import ContentMessages from '../../ContentMessages';
 import dis from "../../dispatcher/dispatcher";
 import filesize from "filesize";
 import { _t } from '../../languageHandler';
-import {Room} from "matrix-js-sdk/src/models/room";
-import {ActionPayload} from "../../dispatcher/payloads";
-import {Action} from "../../dispatcher/actions";
+import { ActionPayload } from "../../dispatcher/payloads";
+import { Action } from "../../dispatcher/actions";
 import ProgressBar from "../views/elements/ProgressBar";
 import AccessibleButton from "../views/elements/AccessibleButton";
-import {IUpload} from "../../models/IUpload";
+import { IUpload } from "../../models/IUpload";
 
 interface IProps {
     room: Room;

From 51ac5421c9848be775c977009454e7f55c79d155 Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Sat, 6 Mar 2021 11:30:31 +0200
Subject: [PATCH 296/389] chore: refactor code

pass only the mxEvent object to ViewSource
derive the necessary values inside the component
---
 src/components/structures/ViewSource.js       | 161 +++++++++---------
 .../views/context_menus/MessageContextMenu.js |   9 +-
 .../views/messages/EditHistoryMessage.js      |   6 +-
 3 files changed, 86 insertions(+), 90 deletions(-)

diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index a31876ea76..369a0a1ddd 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -26,127 +26,134 @@ import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
 
 export default class ViewSource extends React.Component {
     static propTypes = {
-        content: PropTypes.object.isRequired,
         onFinished: PropTypes.func.isRequired,
-        roomId: PropTypes.string.isRequired,
-        eventId: PropTypes.string.isRequired,
-        isEncrypted: PropTypes.bool.isRequired,
-        decryptedContent: PropTypes.object,
-        event: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
+        mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
     };
 
     constructor(props) {
         super(props);
 
         this.state = {
-            editComponent: null,
+            isEditing: false,
         };
     }
 
     onBack() {
-        this.setState({ editComponent: null });
+        this.setState({ isEditing: false });
     }
 
-    editEvent() {
-        const isStateEvent = this.props.event.isState();
-        console.log("isStateEvent", isStateEvent);
-        if (isStateEvent) {
-            this.setState({
-                editComponent: (
-                    <MatrixClientContext.Consumer>
-                        {(cli) => (
-                            <SendCustomEvent
-                                room={cli.getRoom(this.props.roomId)}
-                                forceStateEvent={true}
-                                onBack={() => this.onBack()}
-                                inputs={{
-                                    eventType: this.props.event.getType(),
-                                    evContent: JSON.stringify(this.props.event.getContent(), null, "\t"),
-                                    stateKey: this.props.event.getStateKey(),
-                                }}
-                            />
-                        )}
-                    </MatrixClientContext.Consumer>
-                ),
-            });
-        } else {
-            // send an edit-message event
-            // prefill the "m.new_content" field
-            const originalContent = this.props.event.getContent();
-            const originalEventId = this.props.eventId;
-            const content = {
-                ...originalContent,
-                "m.new_content": originalContent,
-                "m.relates_to": {
-                    rel_type: "m.replace",
-                    event_id: originalEventId,
-                },
-            };
-            this.setState({
-                editComponent: (
-                    <MatrixClientContext.Consumer>
-                        {(cli) => (
-                            <SendCustomEvent
-                                room={cli.getRoom(this.props.roomId)}
-                                forceStateEvent={false}
-                                forceGeneralEvent={true}
-                                onBack={() => this.onBack()}
-                                inputs={{
-                                    eventType: this.props.event.getType(),
-                                    evContent: JSON.stringify(content, null, "\t"),
-                                }}
-                            />
-                        )}
-                    </MatrixClientContext.Consumer>
-                ),
-            });
-        }
+    onEdit() {
+        this.setState({ isEditing: true });
     }
 
-    render() {
-        const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
+    // returns the dialog body for viewing the event source
+    viewSourceContent() {
+        const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
+        const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType();
+        const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private
+        const originalEventSource = mxEvent.event;
 
-        let content;
-        if (this.props.isEncrypted) {
-            content = (
+        if (isEncrypted) {
+            return (
                 <>
                     <details open className="mx_ViewSource_details">
                         <summary>
                             <span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
                         </summary>
-                        <SyntaxHighlight className="json">{JSON.stringify(this.props.decryptedContent, null, 2)}</SyntaxHighlight>
+                        <SyntaxHighlight className="json">{JSON.stringify(decryptedEventSource, null, 2)}</SyntaxHighlight>
                     </details>
                     <details className="mx_ViewSource_details">
                         <summary>
                             <span className="mx_ViewSource_heading">{_t("Original event source")}</span>
                         </summary>
-                        <SyntaxHighlight className="json">{JSON.stringify(this.props.content, null, 2)}</SyntaxHighlight>
+                        <SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
                     </details>
                 </>
             );
         } else {
-            content = (
+            return (
                 <>
                     <div className="mx_ViewSource_heading">{_t("Original event source")}</div>
-                    <SyntaxHighlight className="json">{JSON.stringify(this.props.content, null, 2)}</SyntaxHighlight>
+                    <SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
                 </>
             );
         }
+    }
 
-        const isEditing = this.state.editComponent !== null;
-        console.log(isEditing);
+    // returns the SendCustomEvent component prefilled with the correct details
+    editSourceContent() {
+        const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
 
+        const isStateEvent = mxEvent.isState();
+        console.log("isStateEvent", isStateEvent);
+        const roomId = mxEvent.getRoomId();
+        const eventId = mxEvent.getId();
+        const originalContent = mxEvent.getContent();
+        if (isStateEvent) {
+            return (
+                <MatrixClientContext.Consumer>
+                    {(cli) => (
+                        <SendCustomEvent
+                            room={cli.getRoom(roomId)}
+                            forceStateEvent={true}
+                            onBack={() => this.onBack()}
+                            inputs={{
+                                eventType: mxEvent.getType(),
+                                evContent: JSON.stringify(originalContent, null, "\t"),
+                                stateKey: mxEvent.getStateKey(),
+                            }}
+                        />
+                    )}
+                </MatrixClientContext.Consumer>
+            );
+        } else {
+            // send an edit-message event
+            // prefill the "m.new_content" field
+            const newContent = {
+                ...originalContent,
+                "m.new_content": originalContent,
+                "m.relates_to": {
+                    rel_type: "m.replace",
+                    event_id: eventId,
+                },
+            };
+            return (
+                <MatrixClientContext.Consumer>
+                    {(cli) => (
+                        <SendCustomEvent
+                            room={cli.getRoom(roomId)}
+                            forceStateEvent={false}
+                            forceGeneralEvent={true}
+                            onBack={() => this.onBack()}
+                            inputs={{
+                                eventType: mxEvent.getType(),
+                                evContent: JSON.stringify(newContent, null, "\t"),
+                            }}
+                        />
+                    )}
+                </MatrixClientContext.Consumer>
+            );
+        }
+    }
+
+    render() {
+        const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
+        const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
+
+        const isEditing = this.state.isEditing;
+        const roomId = mxEvent.getRoomId();
+        const eventId = mxEvent.getId();
         return (
             <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
                 <div>
-                    <div className="mx_ViewSource_label_left">Room ID: {this.props.roomId}</div>
-                    <div className="mx_ViewSource_label_left">Event ID: {this.props.eventId}</div>
+                    <div className="mx_ViewSource_label_left">Room ID: {roomId}</div>
+                    <div className="mx_ViewSource_label_left">Event ID: {eventId}</div>
                     <div className="mx_ViewSource_separator" />
-                    {isEditing ? this.state.editComponent : content}
+                    {isEditing ? this.editSourceContent() : this.viewSourceContent()}
                 </div>
                 {!isEditing && (
                     <div className="mx_Dialog_buttons">
-                        <button onClick={() => this.editEvent()}>{_t("Edit")}</button>
+                        <button onClick={() => this.onEdit()}>{_t("Edit")}</button>
                     </div>
                 )}
             </BaseDialog>
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index a1c111b19c..6809d28e36 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -124,16 +124,9 @@ export default class MessageContextMenu extends React.Component {
     };
 
     onViewSourceClick = () => {
-        const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
         const ViewSource = sdk.getComponent('structures.ViewSource');
         Modal.createTrackedDialog('View Event Source', '', ViewSource, {
-            roomId: ev.getRoomId(),
-            eventId: ev.getId(),
-            content: ev.event,
-            event: ev,
-            isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(),
-            // FIXME: _clearEvent is private
-            decryptedContent: ev._clearEvent,
+            mxEvent: this.props.mxEvent,
         }, 'mx_Dialog_viewsource');
         this.closeMenu();
     };
diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
index 68a3c95745..3bd9dfbd21 100644
--- a/src/components/views/messages/EditHistoryMessage.js
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -74,11 +74,7 @@ export default class EditHistoryMessage extends React.PureComponent {
     _onViewSourceClick = () => {
         const ViewSource = sdk.getComponent('structures.ViewSource');
         Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, {
-            roomId: this.props.mxEvent.getRoomId(),
-            eventId: this.props.mxEvent.getId(),
-            content: this.props.mxEvent.event,
-            isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(),
-            decryptedContent: this.props.mxEvent._clearEvent,
+            mxEvent: this.props.mxEvent,
         }, 'mx_Dialog_viewsource');
     };
 

From 29b95e60833fef1bed598897dad2deff4e875ff4 Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Sat, 6 Mar 2021 16:47:29 +0200
Subject: [PATCH 297/389] fix: make edit prefill work correctly from
 EditHistory

handle encrypted and unencrypted events

get the correct event_id (the base message) when called from EditHistoryMessage

keep only the `body` and `msgtype` fields when prefilling
---
 src/components/structures/ViewSource.js | 33 +++++++++++++++++++------
 1 file changed, 26 insertions(+), 7 deletions(-)

diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index 369a0a1ddd..4ee70ee2a7 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -39,6 +39,7 @@ export default class ViewSource extends React.Component {
     }
 
     onBack() {
+        // TODO: refresh the "Event ID:" modal header
         this.setState({ isEditing: false });
     }
 
@@ -80,15 +81,28 @@ export default class ViewSource extends React.Component {
         }
     }
 
+    // returns the id of the initial message, not the id of the previous edit
+    getBaseEventId() {
+        const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
+        const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType();
+        const baseMxEvent = this.props.mxEvent;
+
+        if (isEncrypted) {
+            // `relates_to` field is inside the encrypted event
+            return mxEvent.event.content["m.relates_to"]?.event_id ?? baseMxEvent.getId();
+        } else {
+            return mxEvent.getContent()["m.relates_to"]?.event_id ?? baseMxEvent.getId();
+        }
+    }
+
     // returns the SendCustomEvent component prefilled with the correct details
     editSourceContent() {
         const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
 
         const isStateEvent = mxEvent.isState();
-        console.log("isStateEvent", isStateEvent);
         const roomId = mxEvent.getRoomId();
-        const eventId = mxEvent.getId();
         const originalContent = mxEvent.getContent();
+
         if (isStateEvent) {
             return (
                 <MatrixClientContext.Consumer>
@@ -107,14 +121,19 @@ export default class ViewSource extends React.Component {
                 </MatrixClientContext.Consumer>
             );
         } else {
-            // send an edit-message event
-            // prefill the "m.new_content" field
+            // prefill an edit-message event
+            // keep only the `body` and `msgtype` fields of originalContent
+            const bodyToStartFrom = originalContent["m.new_content"]?.body ?? originalContent.body; // prefill the last edit body, to start editing from there
             const newContent = {
-                ...originalContent,
-                "m.new_content": originalContent,
+                "body": ` * ${bodyToStartFrom}`,
+                "msgtype": originalContent.msgtype,
+                "m.new_content": {
+                    body: bodyToStartFrom,
+                    msgtype: originalContent.msgtype,
+                },
                 "m.relates_to": {
                     rel_type: "m.replace",
-                    event_id: eventId,
+                    event_id: this.getBaseEventId(),
                 },
             };
             return (

From df52ec28d60ecdf9719f3b5339a805f6a643f753 Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Sat, 6 Mar 2021 17:09:46 +0200
Subject: [PATCH 298/389] fix: show edit button only if you have permission

---
 src/components/structures/ViewSource.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index 4ee70ee2a7..ddcffe4f7f 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -23,6 +23,7 @@ import { _t } from "../../languageHandler";
 import * as sdk from "../../index";
 import MatrixClientContext from "../../contexts/MatrixClientContext";
 import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
+import { canEditContent } from "../../utils/EventUtils";
 
 export default class ViewSource extends React.Component {
     static propTypes = {
@@ -162,6 +163,7 @@ export default class ViewSource extends React.Component {
         const isEditing = this.state.isEditing;
         const roomId = mxEvent.getRoomId();
         const eventId = mxEvent.getId();
+        const canEdit = canEditContent(this.props.mxEvent);
         return (
             <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
                 <div>
@@ -170,7 +172,7 @@ export default class ViewSource extends React.Component {
                     <div className="mx_ViewSource_separator" />
                     {isEditing ? this.editSourceContent() : this.viewSourceContent()}
                 </div>
-                {!isEditing && (
+                {!isEditing && canEdit && (
                     <div className="mx_Dialog_buttons">
                         <button onClick={() => this.onEdit()}>{_t("Edit")}</button>
                     </div>

From 1d8c9375cfb511b295dd6228963d8b5d0f687a2e Mon Sep 17 00:00:00 2001
From: libexus <Asterixeins324@gmail.com>
Date: Thu, 4 Mar 2021 20:35:28 +0000
Subject: [PATCH 299/389] Translated using Weblate (German)

Currently translated at 99.3% (2761 of 2780 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 | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 8a07b6cd9f..13fc6fc3b7 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -3070,5 +3070,6 @@
     "Value": "Wert",
     "Setting ID": "Einstellungs-ID",
     "Failed to save settings": "Einstellungen konnten nicht gespeichert werden",
-    "Show chat effects (animations when receiving e.g. confetti)": "Animierte Chateffekte zeigen, wenn z.B. Konfetti-Emojis erhalten werden"
+    "Show chat effects (animations when receiving e.g. confetti)": "Animierte Chateffekte zeigen, wenn z.B. Konfetti-Emojis erhalten werden",
+    "Save setting values": "Einstellungswerte speichern"
 }

From ccb47285bc3030697fd88db50eca7d3742504ac7 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Thu, 4 Mar 2021 20:34:54 +0000
Subject: [PATCH 300/389] Translated using Weblate (German)

Currently translated at 99.3% (2761 of 2780 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 | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 13fc6fc3b7..07a1a4f805 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -3071,5 +3071,6 @@
     "Setting ID": "Einstellungs-ID",
     "Failed to save settings": "Einstellungen konnten nicht gespeichert werden",
     "Show chat effects (animations when receiving e.g. confetti)": "Animierte Chateffekte zeigen, wenn z.B. Konfetti-Emojis erhalten werden",
-    "Save setting values": "Einstellungswerte speichern"
+    "Save setting values": "Einstellungswerte speichern",
+    "Caution:": "Vorsicht:"
 }

From 395c010a84deb26d2fa812022376d2ef22825392 Mon Sep 17 00:00:00 2001
From: iaiz <git@iapellaniz.com>
Date: Sat, 6 Mar 2021 21:01:51 +0000
Subject: [PATCH 301/389] Translated using Weblate (Spanish)

Currently translated at 100.0% (2780 of 2780 strings)

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

diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json
index cbe7e0a223..4b50d59f2a 100644
--- a/src/i18n/strings/es.json
+++ b/src/i18n/strings/es.json
@@ -45,7 +45,7 @@
     "Cryptography": "Criptografía",
     "Current password": "Contraseña actual",
     "/ddg is not a command": "/ddg no es un comando",
-    "Deactivate Account": "Desactivar Cuenta",
+    "Deactivate Account": "Desactivar cuenta",
     "Decrypt %(text)s": "Descifrar %(text)s",
     "Deops user with given id": "Quita el poder de operador al usuario con la ID dada",
     "Default": "Por defecto",
@@ -65,18 +65,18 @@
     "Failed to change power level": "Falló al cambiar de nivel de acceso",
     "Failed to forget room %(errCode)s": "No se pudo olvidar la sala %(errCode)s",
     "Failed to join room": "No se pudo unir a la sala",
-    "Failed to kick": "Falló al expulsar",
+    "Failed to kick": "No se ha podido echar",
     "Failed to leave room": "No se pudo salir de la sala",
-    "Failed to load timeline position": "Falló al cargar el historico",
+    "Failed to load timeline position": "Fallo al cargar el historial",
     "Failed to mute user": "No se pudo silenciar al usuario",
     "Failed to reject invite": "Falló al rechazar invitación",
     "Failed to reject invitation": "Falló al rechazar la invitación",
     "Failed to send email": "No se pudo enviar el correo electrónico",
     "Failed to send request.": "El envío de la solicitud falló.",
-    "Failed to set display name": "No se pudo establecer el nombre público",
+    "Failed to set display name": "No se ha podido cambiar el nombre público",
     "Failed to unban": "No se pudo quitar veto",
-    "Failed to verify email address: make sure you clicked the link in the email": "No se pudo verificar la dirección de correo electrónico: asegúrate de hacer clic en el enlace del correo electrónico",
-    "Failure to create room": "No se pudo crear sala",
+    "Failed to verify email address: make sure you clicked the link in the email": "No se ha podido verificar la dirección de correo electrónico: asegúrate de hacer clic en el enlace del mensaje",
+    "Failure to create room": "No se ha podido crear la sala",
     "Favourite": "Añadir a favoritos",
     "Favourites": "Favoritos",
     "Fill screen": "Llenar pantalla",
@@ -100,10 +100,10 @@
     "Sign in with": "Quiero iniciar sesión con",
     "Join Room": "Unirse a la Sala",
     "%(targetName)s joined the room.": "%(targetName)s se unió a la sala.",
-    "%(senderName)s kicked %(targetName)s.": "%(senderName)s expulsó a %(targetName)s.",
-    "Kick": "Expulsar",
-    "Kicks user with given id": "Expulsa al usuario con la ID dada",
-    "Labs": "Laboratorios",
+    "%(senderName)s kicked %(targetName)s.": "%(senderName)s echó a %(targetName)s.",
+    "Kick": "Echar",
+    "Kicks user with given id": "Echa al usuario con la ID dada",
+    "Labs": "Experimentos",
     "Leave room": "Salir de la sala",
     "%(targetName)s left the room.": "%(targetName)s salió de la sala.",
     "Logout": "Cerrar sesión",
@@ -134,7 +134,7 @@
     "Incorrect username and/or password.": "Nombre de usuario y/o contraseña incorrectos.",
     "Invited": "Invitado",
     "Jump to first unread message.": "Ir al primer mensaje no leído.",
-    "Last seen": "Visto por última vez",
+    "Last seen": "Último uso",
     "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s hizo visible el historial futuro de la sala para todos los miembros de la sala, desde el momento en que son invitados.",
     "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s hizo visible el historial futuro de la sala para todos los miembros de la sala, desde el momento en que se unieron.",
     "%(senderName)s made future room history visible to all room members.": "%(senderName)s hizo visible el historial futuro de la sala para todos los miembros de la sala.",
@@ -173,7 +173,7 @@
     "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s invitó a %(targetDisplayName)s a unirse a la sala.",
     "Server error": "Error del servidor",
     "Server may be unavailable, overloaded, or search timed out :(": "El servidor podría estar saturado o desconectado, o la búsqueda caducó :(",
-    "Server may be unavailable, overloaded, or you hit a bug.": "El servidor podría estar saturado o desconectado, o encontraste un fallo.",
+    "Server may be unavailable, overloaded, or you hit a bug.": "El servidor podría estar saturado o desconectado, o has encontrado un fallo.",
     "Server unavailable, overloaded, or something else went wrong.": "Servidor saturado, desconectado, o alguien ha roto algo.",
     "Session ID": "ID de Sesión",
     "%(senderName)s set a profile picture.": "%(senderName)s estableció una imagen de perfil.",
@@ -182,7 +182,7 @@
     "Signed Out": "Desconectado",
     "Sign in": "Conectar",
     "Sign out": "Cerrar sesión",
-    "%(count)s of your messages have not been sent.|other": "Algunos de sus mensajes no han sido enviados.",
+    "%(count)s of your messages have not been sent.|other": "Algunos de tus mensajes no han sido enviados.",
     "Someone": "Alguien",
     "Start authentication": "Iniciar autenticación",
     "Submit": "Enviar",
@@ -191,8 +191,8 @@
     "Active call (%(roomName)s)": "Llamada activa (%(roomName)s)",
     "Add a topic": "Añadir un tema",
     "No media permissions": "Sin permisos para el medio",
-    "You may need to manually permit %(brand)s to access your microphone/webcam": "Probablemente necesite dar permisos manualmente a %(brand)s para su micrófono/cámara",
-    "Are you sure you want to leave the room '%(roomName)s'?": "¿Seguro que quieres salir de la sala '%(roomName)s'?",
+    "You may need to manually permit %(brand)s to access your microphone/webcam": "Probablemente necesites dar permisos manualmente a %(brand)s para tu micrófono/cámara",
+    "Are you sure you want to leave the room '%(roomName)s'?": "¿Salir de la sala «%(roomName)s?",
     "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.": "No se puede conectar al servidor base. Por favor, comprueba tu conexión, asegúrate de que el <a>certificado SSL del servidor</a> es de confiaza, y comprueba que no haya extensiones de navegador bloqueando las peticiones.",
     "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s eliminó el nombre de la sala.",
     "Drop File Here": "Deje el fichero aquí",
@@ -214,7 +214,7 @@
     "No results": "No hay resultados",
     "No users have specific privileges in this room": "Ningún usuario tiene permisos específicos en esta sala",
     "OK": "Vale",
-    "olm version:": "versión de olm:",
+    "olm version:": "Versión de olm:",
     "Only people who have been invited": "Solo las personas que hayan sido invitadas",
     "Operation failed": "Falló la operación",
     "Password": "Contraseña",
@@ -238,19 +238,19 @@
     "Results from DuckDuckGo": "Resultados desde DuckDuckGo",
     "Return to login screen": "Regresar a la pantalla de inicio de sesión",
     "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s no tiene permiso para enviarte notificaciones - por favor, comprueba los ajustes de tu navegador",
-    "%(brand)s was not given permission to send notifications - please try again": "No se le dio permiso a %(brand)s para enviar notificaciones - por favor, inténtalo nuevamente",
+    "%(brand)s was not given permission to send notifications - please try again": "No le has dado permiso a %(brand)s para enviar notificaciones. Por favor, inténtalo de nuevo",
     "%(brand)s version:": "Versión de %(brand)s:",
     "Room %(roomId)s not visible": "La sala %(roomId)s no está visible",
     "Searches DuckDuckGo for results": "Busca resultados en DuckDuckGo",
     "Show timestamps in 12 hour format (e.g. 2:30pm)": "Mostrar marcas temporales en formato de 12 horas (ej. 2:30pm)",
     "This email address is already in use": "Esta dirección de correo electrónico ya está en uso",
-    "This email address was not found": "No se encontró esta dirección de correo electrónico",
+    "This email address was not found": "No se ha encontrado la dirección de correo electrónico",
     "The email address linked to your account must be entered.": "Debes ingresar la dirección de correo electrónico vinculada a tu cuenta.",
-    "The remote side failed to pick up": "El lado remoto no contestó",
+    "The remote side failed to pick up": "El otro lado no ha respondido a la llamada",
     "This room has no local addresses": "Esta sala no tiene direcciones locales",
     "This room is not recognised.": "No se reconoce esta sala.",
     "This doesn't appear to be a valid email address": "Esto no parece un e-mail váido",
-    "This phone number is already in use": "Este número telefónico ya está en uso",
+    "This phone number is already in use": "Este número de teléfono ya está en uso",
     "This room": "Esta sala",
     "This room is not accessible by remote Matrix servers": "Esta sala no es accesible por otros servidores Matrix",
     "Cancel": "Cancelar",
@@ -308,9 +308,9 @@
     "You are already in a call.": "Ya estás participando en una llamada.",
     "You are not in this room.": "No estás en esta sala.",
     "You do not have permission to do that in this room.": "No tienes permiso para realizar esa acción en esta sala.",
-    "You cannot place a call with yourself.": "No puedes realizar una llamada contigo mismo.",
+    "You cannot place a call with yourself.": "No puedes llamarte a ti mismo.",
     "Cannot add any more widgets": "no es posible agregar mas widgets",
-    "Publish this room to the public in %(domain)s's room directory?": "¿Quieres que esta sala aparezca en el directorio de salas de %(domain)s?",
+    "Publish this room to the public in %(domain)s's room directory?": "¿Quieres que la sala aparezca en el directorio de salas de %(domain)s?",
     "AM": "AM",
     "PM": "PM",
     "The maximum permitted number of widgets have already been added to this room.": "La cantidad máxima de widgets permitida ha sido alcanzada en esta sala.",
@@ -323,20 +323,20 @@
     "You have <a>disabled</a> URL previews by default.": "Has <a>desactivado</a> la vista previa de URLs por defecto.",
     "You have <a>enabled</a> URL previews by default.": "Has <a>activado</a> las vista previa de URLs por defecto.",
     "You have no visible notifications": "No tiene notificaciones visibles",
-    "You must <a>register</a> to use this functionality": "Usted debe ser un <a>registrar</a> para usar esta funcionalidad",
+    "You must <a>register</a> to use this functionality": "<a>Regístrate</a> para usar esta funcionalidad",
     "You need to be able to invite users to do that.": "Debes ser capaz de invitar usuarios para realizar esa acción.",
     "You need to be logged in.": "Necesitas haber iniciado sesión.",
     "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Tu dirección de correo electrónico no parece estar asociada a una ID de Matrix en este servidor base.",
     "You seem to be in a call, are you sure you want to quit?": "Parece estar en medio de una llamada, ¿esta seguro que desea salir?",
     "You seem to be uploading files, are you sure you want to quit?": "Pareces estar subiendo archivos, ¿seguro que quieres salir?",
     "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "No podrás deshacer este cambio porque estás promoviendo al usuario para tener el mismo nivel de autoridad que tú.",
-    "Sun": "Dom",
-    "Mon": "Lun",
-    "Tue": "Mar",
-    "Wed": "Mie",
-    "Thu": "Jue",
-    "Fri": "Vie",
-    "Sat": "Sab",
+    "Sun": "dom.",
+    "Mon": "lun.",
+    "Tue": "mar.",
+    "Wed": "mié.",
+    "Thu": "jue.",
+    "Fri": "vie.",
+    "Sat": "sáb.",
     "Jan": "Ene",
     "Feb": "Feb",
     "Mar": "Mar",
@@ -346,25 +346,25 @@
     "Jul": "Jul",
     "Aug": "Ago",
     "Add rooms to this community": "Agregar salas a esta comunidad",
-    "Call Failed": "La Llamada Falló",
+    "Call Failed": "Llamada fallida",
     "Sep": "Sep",
     "Oct": "Oct",
     "Nov": "Nov",
     "Dec": "Dic",
     "Warning": "Advertencia",
-    "Unpin Message": "Desmarcar Mensaje",
+    "Unpin Message": "Desanclar mensaje",
     "Online": "En línea",
     "Submit debug logs": "Enviar registros de depuración",
     "The platform you're on": "La plataforma en la que te encuentras",
-    "The version of %(brand)s": "La version de %(brand)s",
-    "Your language of choice": "El idioma de tu elección",
+    "The version of %(brand)s": "La versión de %(brand)s",
+    "Your language of choice": "Idioma elegido",
     "Your homeserver's URL": "La URL de tu servidor base",
     "The information being sent to us to help make %(brand)s better includes:": "La información que se nos envía para ayudarnos a mejorar %(brand)s incluye:",
     "Whether or not you're using the Richtext mode of the Rich Text Editor": "Estés utilizando o no el modo de Texto Enriquecido del Editor de Texto Enriquecido",
     "Who would you like to add to this community?": "¿A quién te gustaría añadir a esta comunidad?",
     "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Advertencia: cualquier persona que añadas a una comunidad será públicamente visible a cualquiera que conozca la ID de la comunidad",
     "Invite new community members": "Invita nuevos miembros a la comunidad",
-    "Invite to Community": "Invitar a la Comunidad",
+    "Invite to Community": "Invitar a la comunidad",
     "Which rooms would you like to add to this community?": "¿Qué salas te gustaría añadir a esta comunidad?",
     "Fetching third party location failed": "Falló la obtención de la ubicación de un tercero",
     "I understand the risks and wish to continue": "Entiendo los riesgos y deseo continuar",
@@ -487,7 +487,7 @@
     "Event Type": "Tipo de Evento",
     "No rooms to show": "No hay salas para mostrar",
     "Download this file": "Descargar este archivo",
-    "Pin Message": "Fijar Mensaje",
+    "Pin Message": "Anclar mensaje",
     "Failed to change settings": "Error al cambiar los ajustes",
     "View Community": "Ver la comunidad",
     "Developer Tools": "Herramientas de desarrollo",
@@ -500,13 +500,13 @@
     "Every page you use in the app": "Cada página que utilizas en la aplicación",
     "Your device resolution": "La resolución de tu dispositivo",
     "Which officially provided instance you are using, if any": "Qué instancia proporcionada oficialmente estás utilizando, si estás utilizando alguna",
-    "e.g. %(exampleValue)s": "ej. %(exampleValue)s",
-    "e.g. <CurrentPageURL>": "ej. <CurrentPageURL>",
+    "e.g. %(exampleValue)s": "ej.: %(exampleValue)s",
+    "e.g. <CurrentPageURL>": "ej.: <CurrentPageURL>",
     "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Donde esta página incluya información identificable, como una sala, usuario o ID de grupo, esos datos se eliminan antes de enviarse al servidor.",
-    "Call in Progress": "Llamada en Curso",
+    "Call in Progress": "Llamada en curso",
     "A call is currently being placed!": "¡Se está realizando una llamada en este momento!",
     "A call is already in progress!": "¡Ya hay una llamada en curso!",
-    "Permission Required": "Permiso Requerido",
+    "Permission Required": "Se necesita permiso",
     "You do not have permission to start a conference call in this room": "No tienes permiso para iniciar una llamada de conferencia en esta sala",
     "%(weekDayName)s %(time)s": "%(weekDayName)s a las %(time)s",
     "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s %(day)s de %(monthName)s a las %(time)s",
@@ -529,13 +529,13 @@
     "You are no longer ignoring %(userId)s": "Ya no ignoras a %(userId)s",
     "Opens the Developer Tools dialog": "Abre el diálogo de Herramientas de Desarrollador",
     "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s cambió su nombre público a %(displayName)s.",
-    "%(senderName)s changed the pinned messages for the room.": "%(senderName)s cambió los mensajes con chincheta en la sala.",
+    "%(senderName)s changed the pinned messages for the room.": "%(senderName)s cambió los mensajes anclados de la sala.",
     "%(widgetName)s widget modified by %(senderName)s": "el widget %(widgetName)s fue modificado por %(senderName)s",
     "%(widgetName)s widget added by %(senderName)s": "componente %(widgetName)s añadido por %(senderName)s",
     "%(widgetName)s widget removed by %(senderName)s": "componente %(widgetName)s eliminado por %(senderName)s",
     "Your browser does not support the required cryptography extensions": "Su navegador no soporta las extensiones de criptografía requeridas",
     "Not a valid %(brand)s keyfile": "No es un archivo de claves de %(brand)s válido",
-    "Message Pinning": "Mensajes con chincheta",
+    "Message Pinning": "Mensajes anclados",
     "Always show encryption icons": "Mostrar siempre iconos de cifrado",
     "Automatically replace plain text Emoji": "Reemplazar automáticamente texto por Emojis",
     "Mirror local video feed": "Clonar transmisión de video local",
@@ -556,9 +556,9 @@
     "Kick this user?": "¿Echar a este usuario?",
     "Unban this user?": "¿Quitarle el veto a este usuario?",
     "Ban this user?": "¿Vetar a este usuario?",
-    "Demote yourself?": "¿Degradarse a ud mismo?",
-    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "No podrá deshacer este cambio ya que está degradándose a usted mismo, si es el usuario con menos privilegios de la sala le resultará imposible recuperarlos.",
-    "Demote": "Degradar",
+    "Demote yourself?": "¿Quitarte permisos a ti mismo?",
+    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "No podrás deshacer este cambio ya que estás quitándote permisos a ti mismo, si eres el último usuario con privilegios de la sala te resultará imposible recuperarlos.",
+    "Demote": "Quitar permisos",
     "Unignore": "Dejar de ignorar",
     "Ignore": "Ignorar",
     "Jump to read receipt": "Saltar a recibo leído",
@@ -567,10 +567,10 @@
     "Share Link to User": "Compartir enlace al usuario",
     "Send an encrypted reply…": "Enviar una respuesta cifrada…",
     "Send an encrypted message…": "Enviar un mensaje cifrado…",
-    "Jump to message": "Ir a mensaje",
-    "No pinned messages.": "No hay mensajes con chincheta.",
+    "Jump to message": "Ir al mensaje",
+    "No pinned messages.": "No hay mensajes anclados.",
     "Loading...": "Cargando...",
-    "Pinned Messages": "Mensajes con chincheta",
+    "Pinned Messages": "Mensajes anclados",
     "%(duration)ss": "%(duration)ss",
     "%(duration)sm": "%(duration)sm",
     "%(duration)sh": "%(duration)sh",
@@ -590,18 +590,18 @@
     "Community Invites": "Invitaciones a comunidades",
     "Banned by %(displayName)s": "Vetado por %(displayName)s",
     "Muted Users": "Usuarios silenciados",
-    "Members only (since the point in time of selecting this option)": "Solo miembros (desde el momento en que se selecciona esta opción)",
-    "Members only (since they were invited)": "Solo miembros (desde que fueron invitados)",
-    "Members only (since they joined)": "Solo miembros (desde que se unieron)",
+    "Members only (since the point in time of selecting this option)": "Solo participantes (desde el momento en que se selecciona esta opción)",
+    "Members only (since they were invited)": "Solo participantes (desde que fueron invitados)",
+    "Members only (since they joined)": "Solo participantes (desde que se unieron)",
     "You don't currently have any stickerpacks enabled": "Actualmente no tienes ningún paquete de pegatinas activado",
     "Stickerpack": "Paquete de pegatinas",
     "Hide Stickers": "Ocultar Pegatinas",
-    "Show Stickers": "Mostrar Pegatinas",
+    "Show Stickers": "Pegatinas",
     "Invalid community ID": "ID de comunidad inválida",
     "'%(groupId)s' is not a valid community ID": "'%(groupId)s' no es una ID de comunidad válida",
     "Flair": "Insignia",
     "Showing flair for these communities:": "Mostrar insignias de las siguientes comunidades:",
-    "This room is not showing flair for any communities": "Esta sala no está mostrando insignias para ninguna comunidad",
+    "This room is not showing flair for any communities": "Esta sala no está mostrando insignias de ninguna comunidad",
     "New community ID (e.g. +foo:%(localDomain)s)": "Nueva ID de comunidad (ej. +foo:%(localDomain)s)",
     "URL previews are enabled by default for participants in this room.": "La vista previa de URL se activa por defecto en los participantes de esta sala.",
     "URL previews are disabled by default for participants in this room.": "La vista previa se desactiva por defecto para los participantes de esta sala.",
@@ -617,7 +617,7 @@
     "Copied!": "¡Copiado!",
     "Failed to copy": "Falló la copia",
     "Add an Integration": "Añadir una Integración",
-    "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?": "Está a punto de ir a un sitio de terceros de modo que pueda autenticar su cuenta para usarla con %(integrationsUrl)s. ¿Desea continuar?",
+    "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?": "Estás a punto de ir a un sitio de terceros de modo que pueda autenticar su cuenta para usarla con %(integrationsUrl)s. ¿Quieres continuar?",
     "An email has been sent to %(emailAddress)s": "Se envió un correo electrónico a %(emailAddress)s",
     "Please check your email to continue registration.": "Por favor consulta tu correo electrónico para continuar con el registro.",
     "Token incorrect": "Token incorrecto",
@@ -648,7 +648,7 @@
     "You're not currently a member of any communities.": "Actualmente no formas parte de ninguna comunidad.",
     "Unknown Address": "Dirección desconocida",
     "Delete Widget": "Eliminar Componente",
-    "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Al borrar un widget se elimina para todos usuarios de la sala. ¿Está seguro?",
+    "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Al borrar un widget se elimina para todos usuarios de la sala. ¿Estás seguro?",
     "Failed to remove widget": "Falló la eliminación del widget",
     "An error ocurred whilst trying to remove the widget from the room": "Ocurrió un error mientras se intentaba eliminar el widget de la sala",
     "Minimize apps": "Minimizar apps",
@@ -717,8 +717,8 @@
     "email address": "dirección de correo electrónico",
     "You have entered an invalid address.": "No ha introducido una dirección correcta.",
     "Try using one of the following valid address types: %(validTypesList)s.": "Intente usar uno de los tipos de direcciones válidos: %(validTypesList)s.",
-    "Confirm Removal": "Confirmar Eliminación",
-    "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.": "¿Seguro que quieres eliminar (borrar) este evento? Ten en cuenta que si borras un cambio de nombre o tema de sala, podrías deshacer el cambio.",
+    "Confirm Removal": "Confirmar eliminación",
+    "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.": "¿Seguro que quieres eliminar este evento? Ten en cuenta que, si borras un cambio de nombre o tema de sala, podrías deshacer el cambio.",
     "Community IDs cannot be empty.": "Las IDs de comunidad no pueden estar vacías.",
     "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Las IDs de comunidad sólo pueden contener caracteres a-z, 0-9, ó '=_-./'",
     "Something went wrong whilst creating your community": "Algo fue mal mientras se creaba la comunidad",
@@ -728,8 +728,8 @@
     "Community ID": "ID de Comunidad",
     "example": "ejemplo",
     "Create": "Crear",
-    "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Esto hará que tu cuenta quede permanentemente inutilizable. No podrás iniciar sesión, y nadie podrá volver a registrar la misma ID de usuario. Esto hará que tu cuenta salga de todas las salas en las cuales participa, y eliminará los datos de tu cuenta de tu servidor de identidad. <b>Esta acción es irreversible.</b>",
-    "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Desactivar tu cuenta <b>no hace que por defecto olvidemos los mensajes que has enviado.</b> Si quieres que olvidemos tus mensajes, por favor marca la casilla a continuación.",
+    "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Esto hará que tu cuenta quede permanentemente inutilizable. No podrás iniciar sesión, y nadie podrá volver a registrar la misma ID de usuario. Saldrás de todas salas en las que participas, y eliminará los datos de tu cuenta de tu servidor de identidad. <b>Esta acción es irreversible.</b>",
+    "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Desactivar tu cuenta <b>no hace que por defecto olvidemos los mensajes que has enviado.</b> Si quieres que olvidemos tus mensajes, marca la casilla a continuación, por favor.",
     "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Que olvidemos tus mensajes implica que los mensajes que hayas enviado no se compartirán con ningún usuario nuevo o no registrado, pero aquellos usuarios registrados que ya tengan acceso a estos mensajes seguirán teniendo acceso a su copia.",
     "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Por favor, olvida todos los mensajes enviados al desactivar mi cuenta. (<b>Advertencia:</b> esto provocará que los usuarios futuros vean conversaciones incompletas)",
     "To continue, please enter your password:": "Para continuar, introduce tu contraseña, por favor:",
@@ -784,7 +784,7 @@
     "%(inviter)s has invited you to join this community": "%(inviter)s te invitó a unirte a esta comunidad",
     "Join this community": "Unirse a esta comunidad",
     "Leave this community": "Salir de esta comunidad",
-    "You are an administrator of this community": "Usted es un administrador de esta comunidad",
+    "You are an administrator of this community": "Eres administrador de esta comunidad",
     "You are a member of this community": "Usted es un miembro de esta comunidad",
     "Who can join this community?": "¿Quién puede unirse a esta comunidad?",
     "Everyone": "Todo el mundo",
@@ -793,7 +793,7 @@
     "Description": "Descripción",
     "Community %(groupId)s not found": "No se encontraron %(groupId)s de la comunidad",
     "Failed to load %(groupId)s": "Falló la carga de %(groupId)s",
-    "This room is not public. You will not be able to rejoin without an invite.": "Esta sala no es pública. No podrá volver a unirse sin una invitación.",
+    "This room is not public. You will not be able to rejoin without an invite.": "Esta sala no es pública. No podrás volver a unirte sin una invitación.",
     "Can't leave Server Notices room": "No se puede salir de la sala de avisos del servidor",
     "This room is used for important messages from the Homeserver, so you cannot leave it.": "La sala se usa para mensajes importantes del servidor base, así que no puedes abandonarla.",
     "Terms and Conditions": "Términos y condiciones",
@@ -807,11 +807,11 @@
     "Error whilst fetching joined communities": "Error al recuperar las comunidades a las que estás unido",
     "Create a new community": "Crear una comunidad nueva",
     "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Crear una comunidad para agrupar usuarios y salas. Construye una página de inicio personalizada para destacarla.",
-    "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "No puede enviar ningún mensaje hasta que revise y esté de acuerdo con <consentLink>nuestros términos y condiciones</consentLink>.",
-    "%(count)s of your messages have not been sent.|one": "No se envió su mensaje.",
+    "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "No puedes enviar ningún mensaje hasta que revises y estés de acuerdo con <consentLink>nuestros términos y condiciones</consentLink>.",
+    "%(count)s of your messages have not been sent.|one": "No se ha enviado tu mensaje.",
     "%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Reenviar todo</resendText> o <cancelText>cancelar todo</cancelText> ahora. También puedes seleccionar mensajes individuales para reenviar o cancelar.",
     "%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Reenviar mensaje</resendText> o <cancelText>cancelar mensaje</cancelText> ahora.",
-    "Connectivity to the server has been lost.": "Se perdió la conexión con el servidor.",
+    "Connectivity to the server has been lost.": "Se ha perdido la conexión con el servidor.",
     "Sent messages will be stored until your connection has returned.": "Los mensajes enviados se almacenarán hasta que vuelva la conexión.",
     "Active call": "Llamada activa",
     "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "¡No hay nadie aquí! ¿Le gustaría <inviteText>invitar a otros</inviteText> o <nowarnText>dejar de advertir sobre la sala vacía</nowarnText>?",
@@ -823,8 +823,8 @@
     "Learn more about how we use analytics.": "Más información sobre el uso de los análisis de estadísticas.",
     "Check for update": "Comprobar actualizaciones",
     "Start automatically after system login": "Ejecutar automáticamente después de iniciar sesión en el sistema",
-    "No Audio Outputs detected": "No se detectaron Salidas de Sonido",
-    "Audio Output": "Salida de Sonido",
+    "No Audio Outputs detected": "No se han detectado salidas de sonido",
+    "Audio Output": "Salida de sonido",
     "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Se envió un correo electrónico a %(emailAddress)s. Una vez hayas seguido el enlace que contiene, haz clic a continuación.",
     "Please note you are logging into the %(hs)s server, not matrix.org.": "Por favor, ten en cuenta que estás iniciando sesión en el servidor %(hs)s, y no en matrix.org.",
     "This homeserver doesn't offer any login flows which are supported by this client.": "Este servidor base no ofrece ningún flujo de inicio de sesión soportado por este cliente.",
@@ -849,7 +849,7 @@
     "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Tu mensaje no se ha enviado porque este servidor base ha alcanzado su límite mensual de usuarios activos. Por favor, <a>contacta con el administrador de tu servicio</a> para continuar utilizándolo.",
     "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Tu mensaje no se ha enviado porque este servidor base ha excedido un límite de recursos. Por favor <a>contacta con el administrador de tu servicio</a> para continuar utilizándolo.",
     "Please <a>contact your service administrator</a> to continue using this service.": "Por favor, <a>contacta al administrador de tu servicio</a> para continuar utilizando este servicio.",
-    "System Alerts": "Alertas de Sistema",
+    "System Alerts": "Alertas del sistema",
     "Forces the current outbound group session in an encrypted room to be discarded": "Obliga a que la sesión de salida grupal actual en una sala cifrada se descarte",
     "Sorry, your homeserver is too old to participate in this room.": "Lo sentimos, tu servidor base tiene una versión demasiado antigua como para participar en esta sala.",
     "Please contact your homeserver administrator.": "Por favor, contacta con la administración de tu servidor base.",
@@ -880,7 +880,7 @@
     "Email addresses": "Correos electrónicos",
     "Language and region": "Idioma y región",
     "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "El fichero %(fileName)s supera el tamaño límite del servidor para subidas",
-    "Unable to load! Check your network connectivity and try again.": "¡No es posible cargar! Comprueba tu conexión de red e inténtalo de nuevo.",
+    "Unable to load! Check your network connectivity and try again.": "No se ha podido cargar. Comprueba tu conexión de red e inténtalo de nuevo.",
     "Failed to invite users to the room:": "Fallo al invitar usuarios a la sala:",
     "Upgrades a room to a new version": "Actualiza una sala a una nueva versión",
     "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s actualizó esta sala.",
@@ -888,10 +888,10 @@
     "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s restringió la sala a invitados.",
     "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s ha permitido a los invitados unirse a la sala.",
     "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s ha prohibido que los invitados se unan a la sala.",
-    "%(displayName)s is typing …": "%(displayName)s está escribiendo …",
-    "%(names)s and %(count)s others are typing …|other": "%(names)s y otros %(count)s están escribiendo …",
-    "%(names)s and %(count)s others are typing …|one": "%(names)s y otro están escribiendo …",
-    "%(names)s and %(lastPerson)s are typing …": "%(names)s y %(lastPerson)s están escribiendo …",
+    "%(displayName)s is typing …": "%(displayName)s está escribiendo…",
+    "%(names)s and %(count)s others are typing …|other": "%(names)s y otros %(count)s están escribiendo…",
+    "%(names)s and %(count)s others are typing …|one": "%(names)s y otra persona están escribiendo…",
+    "%(names)s and %(lastPerson)s are typing …": "%(names)s y %(lastPerson)s están escribiendo…",
     "Unrecognised address": "Dirección desconocida",
     "You do not have permission to invite people to this room.": "No tienes permisos para inviitar gente a esta sala.",
     "User %(user_id)s does not exist": "El usuario %(user_id)s no existe",
@@ -1036,9 +1036,9 @@
     "Open Devtools": "Abrir devtools",
     "General": "General",
     "Room Addresses": "Direcciones de sala",
-    "Set a new account password...": "Establecer una nueva contraseña para la cuenta...",
+    "Set a new account password...": "Elegir una nueva contraseña para la cuenta...",
     "Account management": "Gestión de la cuenta",
-    "Deactivating your account is a permanent action - be careful!": "Desactivar tu cuenta es permanente - ¡Cuidado!",
+    "Deactivating your account is a permanent action - be careful!": "Desactivar tu cuenta es permanente. ¡Cuidado!",
     "Credits": "Créditos",
     "For help with using %(brand)s, click <a>here</a>.": "Si necesitas ayuda usando %(brand)s, haz clic <a>aquí</a>.",
     "For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "Si necesitas ayuda usando %(brand)s, haz clic <a>aquí</a> o abre un chat con nuestro bot usando el botón de abajo.",
@@ -1051,7 +1051,7 @@
     "Room list": "Lista de salas",
     "Autocomplete delay (ms)": "Retardo autocompletado (ms)",
     "Roles & Permissions": "Roles y permisos",
-    "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Los cambios que se hagan sobre quien puede leer el historial se aplicarán solo a nuevos mensajes en esta sala. La visibilidad del historial actual no cambiará.",
+    "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Los cambios que se hagan sobre quién puede leer el historial se aplicarán solo a nuevos mensajes. La visibilidad del historial actual no cambiará.",
     "Security & Privacy": "Seguridad y privacidad",
     "Encryption": "Cifrado",
     "Once enabled, encryption cannot be disabled.": "Una vez activado, el cifrado no se puede desactivar.",
@@ -1066,7 +1066,7 @@
     "Securely back up your keys to avoid losing them. <a>Learn more.</a>": "Haz copia de manera segura de tus claves para evitar perderlas. <a>Lee más.</a>",
     "Not now": "Ahora no",
     "Don't ask me again": "No preguntarme más",
-    "Add some now": "Añadir algunos ahora",
+    "Add some now": "Añadir alguno ahora",
     "Main address": "Dirección principal",
     "Room avatar": "Avatar de la sala",
     "Room Name": "Nombre de sala",
@@ -1082,7 +1082,7 @@
     "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 tu historial de chat, debes exportar las claves de la sala antes de salir. Debes volver a la versión actual de %(brand)s para esto",
     "Incompatible Database": "Base de datos incompatible",
     "Continue With Encryption Disabled": "Seguir con cifrado desactivado",
-    "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verifica ese usuario para marcar como confiable. Confiar en usuarios aporta mucha tranquilidad en los mensajes cifrados de extremo a extremo.",
+    "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verifica a este usuario para marcarlo como de confianza. Confiar en usuarios aporta tranquilidad en los mensajes cifrados de extremo a extremo.",
     "Waiting for partner to confirm...": "Esperando que confirme el compañero...",
     "Incoming Verification Request": "Petición de verificación entrante",
     "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s cambió la regla para unirse a %(rule)s",
@@ -1092,12 +1092,12 @@
     "Verify this user by confirming the following emoji appear on their screen.": "Verifica este usuario confirmando que los siguientes emojis aparecen en su pantalla.",
     "Your %(brand)s is misconfigured": "Tu %(brand)s tiene un error de configuración",
     "Whether or not you're logged in (we don't record your username)": "Hayas o no iniciado sesión (no guardamos tu nombre de usuario)",
-    "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Uses o no los 'breadcrumbs' (iconos sobre la lista de salas)",
+    "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Uses o no las «migas de pan» (iconos sobre la lista de salas)",
     "Replying With Files": "Respondiendo con archivos",
     "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "En este momento no es posible responder con un archivo. ¿Te gustaría subir el archivo sin responder?",
-    "The file '%(fileName)s' failed to upload.": "Falló en subir el archivo '%(fileName)s'.",
+    "The file '%(fileName)s' failed to upload.": "La subida del archivo '%(fileName)s' ha fallado.",
     "The server does not support the room version specified.": "El servidor no soporta la versión de sala especificada.",
-    "Name or Matrix ID": "Nombre o Matrix ID",
+    "Name or Matrix ID": "Nombre o ID de Matrix",
     "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Añade ¯\\_(ツ)_/¯ al principio de un mensaje de texto plano",
     "<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>Aviso</b>: Actualizar una sala <i>no migrará automáticamente a sus miembros a la nueva versión de la sala.</i> Incluiremos un enlace a la nueva sala en la versión antigüa de la misma - los miembros tendrán que seguir ese enlace para unirse a la nueva sala.",
     "Changes your display nickname in the current room only": "Cambia tu apodo sólo en la sala actual",
@@ -1129,16 +1129,16 @@
     "Low bandwidth mode": "Modo de ancho de banda bajo",
     "Got It": "Entendido",
     "Scissors": "Tijeras",
-    "Call failed due to misconfigured server": "Llamada fallida debido a la mala configuración del servidor",
+    "Call failed due to misconfigured server": "La llamada ha fallado debido a una mala configuración del servidor",
     "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Por favor, pídele al administrador de tu servidor base (<code>%(homeserverDomain)s</code>) que configure un servidor TURN para que las llamadas funcionen correctamente.",
     "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.": "Como alternativa, puedes intentar usar el servidor público <code>turn.matrix.org</code>, pero éste no será igual de confiable, y compartirá tu dirección IP con ese servidor. También puedes configurar esto en ajustes.",
-    "Try using turn.matrix.org": "Trata de usar turn.matrix.org",
+    "Try using turn.matrix.org": "Probar usando turn.matrix.org",
     "Messages": "Mensajes",
     "Actions": "Acciones",
     "Other": "Otros",
     "Sends a message as plain text, without interpreting it as markdown": "Envía un mensaje como texto estándar, sin interpretarlo como Markdown",
     "You do not have the required permissions to use this command.": "No tienes los permisos requeridos para usar este comando.",
-    "Changes the avatar of the current room": "Cambia el ávatar de la sala actual",
+    "Changes the avatar of the current room": "Cambia la imagen de la sala actual",
     "Use an identity server": "Usar un servidor de identidad",
     "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Usar un servidor de identidad para invitar por correo. Presiona continuar par usar el servidor de identidad por defecto (%(defaultIdentityServerName)s) o adminístralo en Ajustes.",
     "Use an identity server to invite by email. Manage in Settings.": "Usar un servidor de identidad para invitar por correo. Gestiónalo en ajustes.",
@@ -1166,10 +1166,10 @@
     "Show previews/thumbnails for images": "Mostrar vistas previas para las imágenes",
     "When rooms are upgraded": "Cuando las salas son actualizadas",
     "My Ban List": "Mi lista de baneos",
-    "This is your list of users/servers you have blocked - don't leave the room!": "Esta es la lista de usuarios y servidores que ha bloqueado - ¡No deje la sala!",
+    "This is your list of users/servers you have blocked - don't leave the room!": "Esta es la lista de usuarios y/o servidores que has bloqueado. ¡No te salgas de la sala!",
     "Decline (%(counter)s)": "Declinar (%(counter)s)",
     "Accept <policyLink /> to continue:": "Aceptar <policyLink /> para continuar:",
-    "ID": "Identificación",
+    "ID": "ID",
     "Public Name": "Nombre público",
     "Connecting to integration manager...": "Conectando al gestor de integraciones...",
     "Cannot connect to integration manager": "No se puede conectar al gestor de integraciones",
@@ -1201,10 +1201,10 @@
     "%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s actualizó una regla que bloquea a usuarios que coinciden con %(glob)s por %(reason)s",
     "They match": "Coinciden",
     "They don't match": "No coinciden",
-    "To be secure, do this in person or use a trusted way to communicate.": "Para ser seguro, haz esto en persona o usando una forma de comunicación de confianza.",
+    "To be secure, do this in person or use a trusted way to communicate.": "Para mayor seguridad, haz esto en persona o usando una forma de comunicación de confianza.",
     "Lock": "Bloquear",
     "Verify yourself & others to keep your chats safe": "Verifícate y verifica a otros para mantener tus conversaciones seguras",
-    "Other users may not trust it": "Puede que otros usuarios no confíen en ello",
+    "Other users may not trust it": "Puede que otros usuarios no confíen en ella",
     "Upgrade": "Actualizar",
     "Verify": "Verificar",
     "Later": "Más tarde",
@@ -1216,11 +1216,11 @@
     "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.": "Cambiar la contraseña reiniciará cualquier clave de cifrado end-to-end en todas las sesiones, haciendo el historial de conversaciones encriptado ilegible, a no ser que primero exportes tus claves de sala y después las reimportes. En un futuro esto será mejorado.",
     "in memory": "en memoria",
     "not found": "no encontrado",
-    "Identity Server (%(server)s)": "Servidor de Identidad %(server)s",
+    "Identity Server (%(server)s)": "Servidor de identidad %(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.": "Estás usando actualmente <server></server>para descubrir y ser descubierto por contactos existentes que conoces. Puedes cambiar tu servidor de identidad más abajo.",
     "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "Si no quieres usar <server /> para descubrir y ser descubierto por contactos existentes que conoces, introduce otro servidor de identidad más abajo.",
     "Identity Server": "Servidor de Identidad",
-    "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "No estás usando actualmente un servidor de identidad. Para descubrir y ser descubierto por contactos existentes que conoces, introduce uno más abajo.",
+    "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "No estás usando un servidor de identidad ahora mismo. Para descubrir y ser descubierto por contactos existentes que conoces, introduce uno más abajo.",
     "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.": "Desconectarte de tu servidor de identidad significa que no podrás ser descubierto por otros usuarios y no podrás invitar a otros por email o teléfono.",
     "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Usar un servidor de identidad es opcional. Si eliges no usar un servidor de identidad, no podrás ser descubierto por otros usuarios y no podrás invitar a otros por email o teléfono.",
     "Do not use an identity server": "No usar un servidor de identidad",
@@ -1234,7 +1234,7 @@
     "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "Puede que los siguientes usuarios no existan o sean inválidos, y no pueden ser invitados: %(csvNames)s",
     "Recent Conversations": "Conversaciones recientes",
     "Suggestions": "Sugerencias",
-    "Recently Direct Messaged": "Enviado Mensaje Directo recientemente",
+    "Recently Direct Messaged": "Mensajes directos recientes",
     "Go": "Ir",
     "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.": "Has usado %(brand)s anteriormente en %(host)s con carga diferida de usuarios activada. En esta versión la carga diferida está desactivada. Como el caché local no es compatible entre estas dos configuraciones, %(brand)s tiene que resincronizar tu cuenta.",
     "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Si la otra versión de %(brand)s esta todavía abierta en otra pestaña, por favor, ciérrala, ya que usar %(brand)s en el mismo host con la opción de carga diferida activada y desactivada simultáneamente causará problemas.",
@@ -1394,7 +1394,7 @@
     "Send messages": "Enviar mensajes",
     "Invite users": "Invitar usuarios",
     "Change settings": "Cambiar la configuración",
-    "Kick users": "Expulsar usuarios",
+    "Kick users": "Echar usuarios",
     "Ban users": "Bloquear a usuarios",
     "Remove messages": "Eliminar mensajes",
     "Notify everyone": "Notificar a todos",
@@ -1422,17 +1422,17 @@
     "Disconnect from the identity server <idserver />?": "¿Desconectarse del servidor de identidad <idserver />?",
     "Disconnect": "Desconectarse",
     "You should:": "Deberías:",
-    "Use Single Sign On to continue": "Procede con Registro Único para continuar",
-    "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirma la adición de esta dirección de correo electrónico usando el Registro Único para probar tu identidad.",
-    "Single Sign On": "Registro Único",
-    "Confirm adding email": "Confirmar la adición del correo electrónico",
-    "Click the button below to confirm adding this email address.": "Haz clic en el botón de abajo para confirmar la adición de esta dirección de correo electrónico.",
+    "Use Single Sign On to continue": "Continuar con SSO",
+    "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirma la nueva dirección de correo usando SSO para probar tu identidad.",
+    "Single Sign On": "Single Sign On",
+    "Confirm adding email": "Confirmar un nuevo correo electrónico",
+    "Click the button below to confirm adding this email address.": "Haz clic en el botón de abajo para confirmar esta nueva dirección de correo electrónico.",
     "Confirm": "Confirmar",
-    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Confirme la adición de este número de teléfono usando el Registro Único para probar su identidad...",
-    "Confirm adding phone number": "Confirmar la adición del número de teléfono",
-    "Click the button below to confirm adding this phone number.": "Haga clic en el botón de abajo para confirmar la adición de este número de teléfono.",
+    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Confirma el nuevo número de teléfono usando SSO para probar tu identidad.",
+    "Confirm adding phone number": "Confirmar nuevo número de teléfono",
+    "Click the button below to confirm adding this phone number.": "Haz clic en el botón de abajo para confirmar este nuevo número de teléfono.",
     "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Si estés usando %(brand)s en un dispositivo donde una pantalla táctil es el principal mecanismo de entrada",
-    "Whether you're using %(brand)s as an installed Progressive Web App": "Si estás usando %(brand)s como una Aplicación Web Progresiva instalada",
+    "Whether you're using %(brand)s as an installed Progressive Web App": "Si estás usando %(brand)s como una aplicación web progresiva (PWA) instalada",
     "If you cancel now, you won't complete your operation.": "Si cancela ahora, no completará la operación.",
     "Review where you’re logged in": "Revise dónde hizo su registro",
     "New login. Was this you?": "Nuevo inicio de sesión. ¿Has sido tú?",
@@ -1442,7 +1442,7 @@
     "Create Account": "Crear cuenta",
     "Sign In": "Iniciar sesión",
     "Sends a message as html, without interpreting it as markdown": "Envía un mensaje como html, sin interpretarlo en markdown",
-    "Failed to set topic": "No se ha podido establecer el tema",
+    "Failed to set topic": "No se ha podido cambiar el tema",
     "Command failed": "El comando falló",
     "Could not find user in room": "No se ha encontrado el usuario en la sala",
     "Please supply a widget URL or embed code": "Por favor, proporcione una URL del widget o un código de incrustación",
@@ -1457,9 +1457,9 @@
     "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s cambió la dirección principal y las alternativas de esta sala.",
     "%(senderName)s changed the addresses for this room.": "%(senderName)s cambió las direcciones de esta sala.",
     "You signed in to a new session without verifying it:": "Iniciaste una nueva sesión sin verificarla:",
-    "Verify your other session using one of the options below.": "Verifique su otra sesión utilizando una de las siguientes opciones.",
+    "Verify your other session using one of the options below.": "Verificar la otra sesión utilizando una de las siguientes opciones.",
     "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) inició una nueva sesión sin verificarla:",
-    "Ask this user to verify their session, or manually verify it below.": "Pídale a este usuario que verifique su sesión, o verifíquela manualmente a continuación.",
+    "Ask this user to verify their session, or manually verify it below.": "Pídele al usuario que verifique su sesión, o verifícala manualmente a continuación.",
     "Not Trusted": "No es de confianza",
     "Manually Verify by Text": "Verificar manualmente mediante texto",
     "Interactively verify by Emoji": "Verifica interactivamente con unEmoji",
@@ -1473,7 +1473,7 @@
     "Send read receipts for messages (requires compatible homeserver to disable)": "Enviar recibos de lectura de mensajes (requiere un servidor local compatible para desactivarlo)",
     "Manually verify all remote sessions": "Verificar manualmente todas las sesiones remotas",
     "Confirm the emoji below are displayed on both sessions, in the same order:": "Confirma que los emoji de abajo se muestran en el mismo orden en ambas sesiones:",
-    "Verify this session by confirming the following number appears on its screen.": "Verifique esta sesión confirmando que el siguiente número aparece en su pantalla.",
+    "Verify this session by confirming the following number appears on its screen.": "Verifica esta sesión confirmando que el siguiente número aparece en su pantalla.",
     "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Esperando a que su otra sesión, %(deviceName)s (%(deviceId)s), verifica…",
     "Cancelling…": "Anulando…",
     "Verify all your sessions to ensure your account & messages are safe": "Verifica todas tus sesiones abiertas para asegurarte de que tu cuenta y tus mensajes estén seguros",
@@ -1503,8 +1503,8 @@
     "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Confirme eliminar estas sesiones, probando su identidad con el Registro Único.",
     "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Confirme eliminar esta sesión, probando su identidad con el Registro Único.",
     "Confirm deleting these sessions": "Confirmar la eliminación de estas sesiones",
-    "Click the button below to confirm deleting these sessions.|other": "Haga clic en el botón de abajo para confirmar la eliminación de estas sesiones.",
-    "Click the button below to confirm deleting these sessions.|one": "Haga clic en el botón de abajo para confirmar la eliminación de esta sesión.",
+    "Click the button below to confirm deleting these sessions.|other": "Haz clic en el botón de abajo para confirmar la eliminación de estas sesiones.",
+    "Click the button below to confirm deleting these sessions.|one": "Haz clic en el botón de abajo para confirmar la eliminación de esta sesión.",
     "Delete sessions|other": "Eliminar sesiones",
     "Delete sessions|one": "Eliminar sesión",
     "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Verificar individualmente cada sesión utilizada por un usuario para marcarla como de confianza, no confiando en dispositivos de firma cruzada.",
@@ -1525,7 +1525,7 @@
     "Backup has a <validity>valid</validity> signature from <verify>unverified</verify> session <device></device>": "La copia de seguridad tiene una firma de <validity>válida</validity> de sesión <verify>no verificada</verify> <device></device>",
     "Backup has an <validity>invalid</validity> signature from <verify>verified</verify> session <device></device>": "La copia de seguridad tiene una firma de <validity>no válida</validity> de sesión <verify>verificada</verify> <device></device>",
     "Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> session <device></device>": "La copia de seguridad tiene una firma de <validity>no válida</validity> de sesión <verify>no verificada</verify> <device></device>",
-    "<a>Upgrade</a> to your own domain": "<a>Contratar</a> el uso de un dominio personalizado",
+    "<a>Upgrade</a> to your own domain": "<a>Contratar</a> dominio personalizado",
     "Identity Server URL must be HTTPS": "La URL del servidor de identidad debe ser tipo HTTPS",
     "Not a valid Identity Server (status code %(code)s)": "No es un servidor de identidad válido (código de estado %(code)s)",
     "Could not connect to Identity Server": "No se ha podido conectar al servidor de identidad",
@@ -1546,7 +1546,7 @@
     "Add theme": "Añadir tema",
     "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Para informar de un problema de seguridad relacionado con Matrix, por favor lea <a>Security Disclosure Policy</a> de Matrix.or.",
     "Keyboard Shortcuts": "Atajos de teclado",
-    "Customise your experience with experimental labs features. <a>Learn more</a>.": "Personaliza tu experiencia con las funciones de los laboratorios experimentales. <a>Learn more</a>.",
+    "Customise your experience with experimental labs features. <a>Learn more</a>.": "Personaliza tu experiencia con funciones experimentales. <a>Más información</a>.",
     "Something went wrong. Please try again or view your console for hints.": "Algo salió mal. Por favor, inténtalo de nuevo o mira tu consola para encontrar pistas.",
     "Please try again or view your console for hints.": "Por favor, inténtalo de nuevo o mira tu consola para encontrar pistas.",
     "Ban list rules - %(roomName)s": "Reglas de la lista negra - %(roomName)s",
@@ -1568,7 +1568,7 @@
     "Accept all %(invitedRooms)s invites": "Aceptar todas las invitaciones de %(invitedRooms)s",
     "Cross-signing": "Firma cruzada",
     "Where you’re logged in": "Sesiones",
-    "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Administre los nombres de sus sesiones y salga de las sesiones abajo o <a>verifíquelos en su Perfil de Usuario</a>.",
+    "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Gestiona los nombres de sus sesiones y ciérralas abajo o <a>verifícalas en tu perfil de usuario</a>.",
     "A session's public name is visible to people you communicate with": "El nombre público de una sesión es visible para las personas con las que te comunicas",
     "This room is bridging messages to the following platforms. <a>Learn more.</a>": "Esta sala está haciendo puente con las siguientes plataformas. <a>Aprende más.</a>",
     "This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "Esta sala no está haciendo puente con ninguna plataforma. <a>Aprende más</a>",
@@ -1577,7 +1577,7 @@
     "Reset": "Resetear",
     "Unable to revoke sharing for email address": "No se logró revocar el compartir para la dirección de correo electrónico",
     "Unable to share email address": "No se logró compartir la dirección de correo electrónico",
-    "Click the link in the email you received to verify and then click continue again.": "Haga clic en el enlace del correo electrónico que recibió para verificar y luego nuevamente haga clic en continuar.",
+    "Click the link in the email you received to verify and then click continue again.": "Haz clic en el enlace del correo electrónico para verificar, y luego nuevamente haz clic en continuar.",
     "Revoke": "Revocar",
     "Discovery options will appear once you have added an email above.": "Las opciones de descubrimiento aparecerán una vez que haya añadido un correo electrónico arriba.",
     "Unable to revoke sharing for phone number": "No se logró revocar el intercambio de un número de teléfono",
@@ -1645,14 +1645,14 @@
     "Create a public room": "Crear una sala pública",
     "Create a private room": "Crear una sala privada",
     "Topic (optional)": "Tema (opcional)",
-    "Make this room public": "Convierte esta sala en pública",
+    "Make this room public": "Haz la sala pública",
     "Hide advanced": "Ocultar ajustes avanzados",
     "Show advanced": "Mostrar ajustes avanzados",
     "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Evitar que usuarios de otros servidores Matrix se unan a esta sala (¡Este ajuste no puede ser cambiada más tarde!)",
     "Server did not require any authentication": "El servidor no requirió ninguna autenticación",
     "Server did not return valid authentication information.": "El servidor no devolvió información de autenticación válida.",
     "Confirm your account deactivation by using Single Sign On to prove your identity.": "Confirme la desactivación de su cuenta, usando Registro Único para probar su identidad.",
-    "Are you sure you want to deactivate your account? This is irreversible.": "¿Está seguro de que quiere desactivar su cuenta? Es irreversible.",
+    "Are you sure you want to deactivate your account? This is irreversible.": "¿Estás seguro de que quieres desactivar su cuenta? No se puede deshacer.",
     "Confirm account deactivation": "Confirmar la desactivación de la cuenta",
     "There was a problem communicating with the server. Please try again.": "Hubo un problema de comunicación con el servidor. Por favor, inténtelo de nuevo.",
     "Verify session": "Verificar sesión",
@@ -1660,8 +1660,8 @@
     "Session key": "Código de sesión",
     "View Servers in Room": "Ver servidores en la sala",
     "Verification Requests": "Solicitudes de verificación",
-    "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Verificar que este usuario marcará su sesión como de confianza, y también que marcará su sesión como de confianza para él.",
-    "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Verifique este dispositivo para marcarlo como confiable. Confiar en este dispositivo le da a usted y a otros usuarios tranquilidad adicional cuando utilizan mensajes cifrados de extremo a extremo.",
+    "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Verificar este usuario marcará su sesión como de confianza, y también marcará tu sesión como de confianza para él.",
+    "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Verifica este dispositivo para marcarlo como confiable. Confiar en este dispositivo te da a ti y a otros usuarios tranquilidad adicional cuando utilizáis mensajes cifrados de extremo a extremo.",
     "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "La verificación de este dispositivo lo marcará como de confianza. Los usuarios que te han verificado confiarán en este dispositivo.",
     "Integrations are disabled": "Las integraciones están desactivadas",
     "Enable 'Manage Integrations' in Settings to do this.": "Activa «Gestionar integraciones» en ajustes para hacer esto.",
@@ -1678,23 +1678,23 @@
     "%(brand)s encountered an error during upload of:": "%(brand)s encontró un error durante la carga de:",
     "End": "Fin",
     "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Una vez activado, el cifrado de una sala no puede desactivarse. Los mensajes enviados a una sala cifrada no pueden ser vistos por el servidor, solo lo verán los participantes de la sala. Activar el cifrado puede hacer que muchos bots y bridges no funcionen correctamente. <a>Más información sobre el cifrado</a>",
-    "Joining room …": "Uniéndose a sala …",
-    "Loading …": "Cargando …",
-    "Rejecting invite …": "Rechazando invitación …",
+    "Joining room …": "Uniéndote a sala…",
+    "Loading …": "Cargando…",
+    "Rejecting invite …": "Rechazando invitación…",
     "Join the conversation with an account": "Unirse a la conversación con una cuenta",
     "Sign Up": "Registrarse",
-    "You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s te ha explusado de la sala %(roomName)s",
+    "You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s te ha echado de la sala %(roomName)s",
     "Reason: %(reason)s": "Razón: %(reason)s",
     "Forget this room": "Olvidar esta sala",
-    "Re-join": "Re-entrar",
-    "You were banned from %(roomName)s by %(memberName)s": "%(memberName)s te ha expulsado de %(roomName)s",
+    "Re-join": "Volver a entrar",
+    "You were banned from %(roomName)s by %(memberName)s": "%(memberName)s te ha echado de %(roomName)s",
     "Something went wrong with your invite to %(roomName)s": "Algo salió a mal invitando a %(roomName)s",
     "You can only join it with a working invite.": "Sólo puedes unirte con una invitación que funciona.",
     "Try to join anyway": "Intentar unirse de todas formas",
     "You can still join it because this is a public room.": "Todavía puedes unirte, ya que es una sala pública.",
     "Join the discussion": "Unirse a la sala",
     "Do you want to chat with %(user)s?": "¿Quieres chatear con %(user)s?",
-    "Do you want to join %(roomName)s?": "¿Quieres unirte a la sala %(roomName)s?",
+    "Do you want to join %(roomName)s?": "¿Quieres unirte a %(roomName)s?",
     "<userName/> invited you": "<userName/> te ha invitado",
     "You're previewing %(roomName)s. Want to join it?": "Estás previsualizando %(roomName)s. ¿Quieres unirte?",
     "%(roomName)s can't be previewed. Do you want to join it?": "La sala %(roomName)s no permite previsualización. ¿Quieres unirte?",
@@ -1712,25 +1712,25 @@
     "No recent messages by %(user)s found": "No se han encontrado mensajes recientes de %(user)s",
     "Try scrolling up in the timeline to see if there are any earlier ones.": "Intente desplazarse hacia arriba en la línea de tiempo para ver si hay alguna anterior.",
     "Remove recent messages by %(user)s": "Eliminar mensajes recientes de %(user)s",
-    "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Estás a punto de eliminar %(count)s mensajes de %(user)s. Esto no se puede deshacer. ¿Desea continuar?",
-    "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Estás a punto de eliminar 1 mensaje de %(user)s. Esto no se puede deshacer. ¿Desea continuar?",
-    "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Para una gran cantidad de mensajes, esto podría llevar algún tiempo. Por favor, no refresque a su cliente mientras tanto.",
+    "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Estás a punto de eliminar %(count)s mensajes de %(user)s. No se puede deshacer. ¿Quieres continuar?",
+    "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Estás a punto de eliminar 1 mensaje de %(user)s. No se puede deshacer. ¿Quieres continuar?",
+    "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Para una gran cantidad de mensajes, esto podría llevar algún tiempo. Por favor, no recargues tu aplicación mientras tanto.",
     "Remove %(count)s messages|other": "Eliminar %(count)s mensajes",
     "Remove %(count)s messages|one": "Eliminar 1 mensaje",
     "Deactivate user?": "¿Desactivar usuario?",
-    "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?": "Desactivando a este usuario, este será desconectado y no podrá volver a ingresar. Además, saldrá de todas las salas a que se había unido. Esta acción no puede ser revertida. ¿Está seguro de desactivar este usuario?",
+    "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?": "Desactivar este usuario le cerrará la sesión y desconectará. No podrá volver a iniciar sesión. Además, saldrá de todas las salas a que se había unido. Esta acción no puede deshacerse. ¿Desactivar este usuario?",
     "Deactivate user": "Desactivar usuario",
     "Failed to deactivate user": "Error en desactivar usuario",
     "Remove recent messages": "Eliminar mensajes recientes",
     "Send a reply…": "Enviar una respuesta …",
     "Send a message…": "Enviar un mensaje…",
     "Bold": "Negrita",
-    "Italics": "Cursivo",
+    "Italics": "Cursiva",
     "Strikethrough": "Tachado",
     "Code block": "Bloque de código",
     "Room %(name)s": "Sala %(name)s",
     "Recent rooms": "Salas recientes",
-    "Direct Messages": "Mensaje Directo",
+    "Direct Messages": "Mensajes directos",
     "Loading room preview": "Cargando vista previa de la sala",
     "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Un código de error (%(errcode)s) fue devuelto al tratar de validar su invitación. Podrías intentar pasar esta información a un administrador de la sala.",
     "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Esta invitación a la sala %(roomName)s fue enviada a %(email)s que no está asociada a su cuenta",
@@ -1739,7 +1739,7 @@
     "Use an identity server in Settings to receive invites directly in %(brand)s.": "Utilice un servidor de identidad en Configuración para recibir invitaciones directamente en %(brand)s.",
     "Share this email in Settings to receive invites directly in %(brand)s.": "Comparte este correo electrónico en Configuración para recibir invitaciones directamente en %(brand)s.",
     "<userName/> wants to chat": "<userName/> quiere chatear",
-    "Start chatting": "Empieza a chatear",
+    "Start chatting": "Empieza una conversación",
     "Reject & Ignore user": "Rechazar e ignorar usuario",
     "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Actualizar esta sala cerrará la instancia actual de la sala y creará una sala actualizada con el mismo nombre.",
     "This room has already been upgraded.": "Esta sala ya ha sido actualizada.",
@@ -1760,7 +1760,7 @@
     "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Hubo un error al actualizar la dirección alternativa de la sala. Posiblemente el servidor no lo permita o se produjo un error temporal.",
     "Local address": "Dirección local",
     "Published Addresses": "Direcciones publicadas",
-    "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.": "Las direcciones publicadas pueden ser usadas por cualquier usuario en cualquier servidor para unirse a tu salas. Para publicar una dirección, primero hay que establecerla como una dirección local.",
+    "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.": "Las direcciones publicadas pueden ser usadas por cualquier usuario en cualquier servidor para unirse a tu salas. Para publicar una dirección, primero hay que establecerla como dirección local.",
     "Other published addresses:": "Otras direcciones publicadas:",
     "No other published addresses yet, add one below": "Todavía no hay direcciones publicadas, puedes añadir una más abajo",
     "New published address (e.g. #alias:server)": "Nueva dirección publicada (p.ej.. #alias:server)",
@@ -1773,12 +1773,12 @@
     "Accepting…": "Aceptando…",
     "Start Verification": "Iniciar verificación",
     "Messages in this room are end-to-end encrypted.": "Los mensajes de esta sala están cifrados de extremo a extremo.",
-    "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Sus mensajes son seguros y sólo usted y el destinatario tienen las claves únicas para desbloquearlos.",
+    "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Los mensajes son seguros y sólo tú y el destinatario tienen las claves únicas para desbloquearlos.",
     "Messages in this room are not end-to-end encrypted.": "Los mensajes en esta sala no están cifrados de extremo a extremo.",
     "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "En las salas cifradas, tus mensajes están seguros y solo tú y el destinatario tienen las claves únicas para desbloquearlos.",
     "Verify User": "Verificar usuario",
-    "For extra security, verify this user by checking a one-time code on both of your devices.": "Para mayor seguridad, verifique este usuario comprobando un código temporal vez en ambos dispositivos.",
-    "Your messages are not secure": "Sus mensajes no son seguros",
+    "For extra security, verify this user by checking a one-time code on both of your devices.": "Para mayor seguridad, verifica a este usuario comprobando un código temporal vez dos de tus dispositivos.",
+    "Your messages are not secure": "Los mensajes no son seguros",
     "One of the following may be compromised:": "Uno de los siguientes puede estar comprometido:",
     "Your homeserver": "Tu servidor base",
     "The homeserver the user you’re verifying is connected to": "El servidor base del usuario a que está verificando está conectado a",
@@ -1807,15 +1807,15 @@
     "Verify all users in a room to ensure it's secure.": "Verifica a todos los usuarios de una sala para asegurar que es segura.",
     "In encrypted rooms, verify all users to ensure it’s secure.": "En las salas cifrar, verificar a todos los usuarios para asegurarse de son seguras.",
     "You've successfully verified %(deviceName)s (%(deviceId)s)!": "¡Has verificado con éxito los %(deviceName)s (%(deviceId)s)!",
-    "You've successfully verified %(displayName)s!": "¡Has verificado con éxito los %(displayName)s!",
+    "You've successfully verified %(displayName)s!": "¡Has verificado con éxito a %(displayName)s!",
     "Verified": "Verificado",
-    "Got it": "Lo he entendido",
+    "Got it": "Aceptar",
     "Start verification again from the notification.": "Inicie la verificación nuevamente a partir de la notificación.",
     "Start verification again from their profile.": "Empieza la verificación de nuevo desde su perfil.",
     "Verification timed out.": "El tiempo máximo para la verificación se ha agotado.",
     "You cancelled verification on your other session.": "Canceló la verificación de su otra sesión.",
     "%(displayName)s cancelled verification.": "%(displayName)s canceló la verificación.",
-    "You cancelled verification.": "Usted canceló la verificación.",
+    "You cancelled verification.": "Has cancelado la verificación.",
     "Verification cancelled": "Verificación cancelada",
     "Compare emoji": "Comparar emoji",
     "Encryption enabled": "Cifrado activado",
@@ -1826,33 +1826,33 @@
     "Message Actions": "Acciones de mensaje",
     "Show image": "Mostrar imagen",
     "You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "Ha ignorado a este usuario, así que su mensaje se ha ocultado. <a>Mostrar de todos modos.</a>",
-    "You verified %(name)s": "Usted verificó %(name)s",
-    "You cancelled verifying %(name)s": "Usted canceló la verificación de %(name)s",
+    "You verified %(name)s": "Has verificado a %(name)s",
+    "You cancelled verifying %(name)s": "Has cancelado la verificación de %(name)s",
     "%(name)s cancelled verifying": "%(name)s canceló la verificación",
-    "You accepted": "Usted aceptó",
+    "You accepted": "Aceptaste",
     "%(name)s accepted": "%(name)s aceptó",
-    "You declined": "Usted declinó",
-    "You cancelled": "Usted canceló",
+    "You declined": "Declinaste",
+    "You cancelled": "Cancelaste",
     "%(name)s declined": "%(name)s declinó",
     "%(name)s cancelled": "%(name)s canceló",
     "Accepting …": "Aceptando…",
     "Declining …": "Declinando…",
     "%(name)s wants to verify": "%(name)s quiere verificar",
-    "You sent a verification request": "Usted envió una solicitud de verificación",
+    "You sent a verification request": "Has enviado solicitud de verificación",
     "Show all": "Mostrar todo",
     "Reactions": "Reacciones",
     "<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reaccionó con %(content)s</reactedWith>",
     "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith> reaccionó con %(shortName)s</reactedWith>",
     "Message deleted": "Mensaje eliminado",
     "Message deleted by %(name)s": "Mensaje eliminado por %(name)s",
-    "Edited at %(date)s. Click to view edits.": "Editado el día %(date)s. Haga clic para ver las ediciones.",
+    "Edited at %(date)s. Click to view edits.": "Editado el día %(date)s. Haz clic para ver las ediciones.",
     "edited": "editado",
     "Can't load this message": "No puedo cargar este mensaje",
     "Submit logs": "Enviar registros",
-    "Frequently Used": "Usado con frecuencia",
+    "Frequently Used": "Frecuente",
     "Smileys & People": "Caritas y personas",
     "Animals & Nature": "Animales y naturaleza",
-    "Food & Drink": "Comidas y bebidas",
+    "Food & Drink": "Comida y bebida",
     "Activities": "Actividades",
     "Travel & Places": "Viajes y lugares",
     "Objects": "Objetos",
@@ -1862,17 +1862,17 @@
     "Cancel search": "Cancelar búsqueda",
     "Any of the following data may be shared:": "Cualquiera de los siguientes datos puede ser compartido:",
     "Your display name": "Su nombre mostrado",
-    "Your avatar URL": "La URL de su avatar",
-    "Your user ID": "Su identificación (ID) de usuario",
+    "Your avatar URL": "La URL de tu avatar",
+    "Your user ID": "Tu ID de usuario",
     "Your theme": "Su tema",
     "%(brand)s URL": "URL de %(brand)s",
     "Room ID": "Identidad (ID) de la sala",
-    "Widget ID": "Identificación (ID) de widget",
-    "Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Usar este widget puede resultar en compartir datos <helpIcon /> con %(widgetDomain)s y su Administrador de Integración.",
-    "Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Usar este widget puede resultar en compartir datos <helpIcon /> con %(widgetDomain)s.",
+    "Widget ID": "ID del widget",
+    "Using this widget may share data <helpIcon /> with %(widgetDomain)s & your Integration Manager.": "Usar este widget puede resultar en que se compartan datos <helpIcon /> con %(widgetDomain)s y su administrador de integración.",
+    "Using this widget may share data <helpIcon /> with %(widgetDomain)s.": "Usar este widget puede resultar en que se compartan datos <helpIcon /> con %(widgetDomain)s.",
     "Widgets do not use message encryption.": "Los widgets no utilizan el cifrado de mensajes.",
     "Widget added by": "Widget añadido por",
-    "This widget may use cookies.": "Este widget posiblemente utilice cookies.",
+    "This widget may use cookies.": "Puede que el widget use cookies.",
     "Maximize apps": "Maximizar apps",
     "More options": "Mas opciones",
     "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Por favor, <newIssueLink>crea un nuevo nodo </newIssueLink> en GitHub para que podamos investigar este error.",
@@ -1883,7 +1883,7 @@
     "Signature upload success": "Subida de firma exitosa",
     "Signature upload failed": "Subida de firma falló",
     "Confirm by comparing the following with the User Settings in your other session:": "Confirme comparando lo siguiente con los ajustes de usuario de su otra sesión:",
-    "Confirm this user's session by comparing the following with their User Settings:": "Confirme la sesión de este usuario comparando lo siguiente con la configuración de usuario de él/ella:",
+    "Confirm this user's session by comparing the following with their User Settings:": "Confirma la sesión de este usuario comparando lo siguiente con su configuración:",
     "If they don't match, the security of your communication may be compromised.": "Si no coinciden, la seguridad de su comunicación puede estar comprometida.",
     "Your homeserver doesn't seem to support this feature.": "Tu servidor base no parece soportar esta funcionalidad.",
     "Your account is not secure": "Su cuenta no es segura",
@@ -1895,7 +1895,7 @@
     "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.": "Reportar este mensaje enviará su único 'event ID' al administrador de tu servidor base. Si los mensajes en esta sala están cifrados, el administrador de tu servidor no podrá leer el texto del mensaje ni ver ningún archivo o imagen.",
     "Command Help": "Ayuda del comando",
     "Integration Manager": "Administrador de integración",
-    "Verify other session": "Verifique otra sesión",
+    "Verify other session": "Verificar otra sesión",
     "Verification Request": "Solicitud de verificación",
     "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.": "Un widget localizado en %(widgetUrl)s desea verificar su identidad. Permitiendo esto, el widget podrá verificar su identidad de usuario, pero no realizar acciones como usted.",
     "Enter recovery passphrase": "Introduzca la contraseña de recuperación",
@@ -1927,11 +1927,11 @@
     "Resend removal": "Reenviar la eliminación",
     "Share Permalink": "Compartir enlace",
     "Report Content": "Reportar contenido",
-    "Notification settings": "Configuración de notificaciones",
+    "Notification settings": "Notificaciones",
     "Clear status": "Borrar estado",
     "Update status": "Actualizar estado",
-    "Set status": "Establecer estado",
-    "Set a new status...": "Establecer un estado nuevo...",
+    "Set status": "Cambiar estado",
+    "Set a new status...": "Elegir un estado nuevo...",
     "Hide": "Ocultar",
     "Help": "Ayuda",
     "Reload": "Recargar",
@@ -1987,11 +1987,11 @@
     "Sign in with SSO": "Ingrese con SSO",
     "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Por favor, instale <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, o <safariLink>Safari</safariLink> para la mejor experiencia.",
     "Couldn't load page": "No pude cargar la página",
-    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Usted es un administrador de esta comunidad. No podrás volver a unirte sin una invitación de otro administrador.",
+    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Eres un administrador de esta comunidad. No podrás volver a unirte sin una invitación de otro administrador.",
     "Want more than a community? <a>Get your own server</a>": "¿Quieres más que una comunidad? <a>Obtenga su propio servidor</a>",
     "This homeserver does not support communities": "Este servidor base no permite las comunidades",
-    "Welcome to %(appName)s": "Bienvenido a %(appName)s",
-    "Liberate your communication": "Libere su comunicación",
+    "Welcome to %(appName)s": "Te damos la bienvenida a %(appName)s",
+    "Liberate your communication": "Libera tu comunicación",
     "Send a Direct Message": "Envía un mensaje directo",
     "Explore Public Rooms": "Explorar salas públicas",
     "Create a Group Chat": "Crear un chat grupal",
@@ -2005,7 +2005,7 @@
     "The homeserver may be unavailable or overloaded.": "Es posible el servidor de base no esté disponible o esté sobrecargado.",
     "Preview": "Ver",
     "View": "Ver",
-    "Find a room…": "Encuentre una sala…",
+    "Find a room…": "Encuentrar una sala…",
     "Find a room… (e.g. %(exampleRoom)s)": "Encuentra una sala... (ej.: %(exampleRoom)s)",
     "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Si no encuentras la sala que buscas, pide que te inviten a ella o <a>crea una nueva</a>.",
     "Explore rooms": "Explorar salas",
@@ -2014,7 +2014,7 @@
     "Guest": "Invitado",
     "Your profile": "Su perfil",
     "Could not load user profile": "No se pudo cargar el perfil de usuario",
-    "Verify this login": "Verifique este inicio de sesión",
+    "Verify this login": "Verificar este inicio de sesión",
     "Session verified": "Sesión verificada",
     "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.": "Cambiar la contraseña restablecerá cualquier clave de cifrado de extremo a extremo en todas sus sesiones, haciendo ilegible el historial de chat cifrado. Configura la copia de seguridad de las claves o exporta las claves de la sala de otra sesión antes de restablecer la contraseña.",
     "Your Matrix account on %(serverName)s": "Su cuenta de Matrix en %(serverName)s",
@@ -2077,7 +2077,7 @@
     "%(senderName)s changed the room name": "%(senderName)s cambio el nombre de la sala",
     "You invited %(targetName)s": "Has invitado a %(targetName)s",
     "Are you sure you want to cancel entering passphrase?": "¿Estas seguro que quieres cancelar el ingresar tu contraseña de recuperación?",
-    "Go Back": "No cancelar",
+    "Go Back": "Volver",
     "Joins room with given address": "Entrar a la sala con la dirección especificada",
     "Unrecognised room address:": "No se encuentra la dirección de la sala:",
     "Opens chat with the given user": "Abrir una conversación con el usuario especificado",
@@ -2107,7 +2107,7 @@
     "System font name": "Nombre de la fuente",
     "Enable experimental, compact IRC style layout": "Activar el diseño experimental de IRC compacto",
     "Uploading logs": "Subiendo registros",
-    "Downloading logs": "Descargando registro",
+    "Downloading logs": "Descargando registros",
     "Incoming voice call": "Llamada de voz entrante",
     "Incoming video call": "Videollamada entrante",
     "Incoming call": "Llamada entrante",
@@ -2125,16 +2125,16 @@
     "Modern": "Moderno",
     "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Introduce el nombre de la fuente instalada en tu sistema y %(brand)s intentará utilizarla.",
     "Customise your appearance": "Personaliza la apariencia",
-    "Appearance Settings only affect this %(brand)s session.": "Cambiar las opciones de apariencia solo afecta esta %(brand)s sesión.",
+    "Appearance Settings only affect this %(brand)s session.": "Cambiar las opciones de apariencia solo afecta a esta sesión de %(brand)s.",
     "Please verify the room ID or address and try again.": "Por favor, verifica la ID o dirección de esta sala e inténtalo de nuevo.",
     "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "El administrador del servidor base ha desactivado el cifrado de extremo a extremo en salas privadas y mensajes directos.",
-    "To link to this room, please add an address.": "Para vincular esta sala, por favor añade una dirección.",
+    "To link to this room, please add an address.": "Para obtener un enlace a esta sala, añade una dirección.",
     "The authenticity of this encrypted message can't be guaranteed on this device.": "La autenticidad de este mensaje cifrado no puede ser garantizada en este dispositivo.",
     "No recently visited rooms": "No hay salas visitadas recientemente",
     "People": "Gente",
     "Explore public rooms": "Buscar salas públicas",
     "Can't see what you’re looking for?": "¿No encuentras nada de lo que buscas?",
-    "Explore all public rooms": "Buscar todas las salas publicas",
+    "Explore all public rooms": "Explorar salas públicas",
     "%(count)s results|other": "%(count)s resultados",
     "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Antepone ( ͡° ͜ʖ ͡°) a un mensaje de texto",
     "Group call modified by %(senderName)s": "Llamada grupal modificada por %(senderName)s",
@@ -2204,7 +2204,7 @@
     "You can only pin 2 apps at a time": "Solo puedes anclar 2 aplicaciones a la vez",
     "Message deleted on %(date)s": "Mensaje eliminado el %(date)s",
     "Edited at %(date)s": "Editado el %(date)s",
-    "Click to view edits": "Haga clic para ver las ediciones",
+    "Click to view edits": "Haz clic para ver las ediciones",
     "Categories": "Categorías",
     "Information": "Información",
     "QR Code": "Código QR",
@@ -2222,24 +2222,24 @@
     "Invite people to join %(communityName)s": "Invita a personas a unirse %(communityName)s",
     "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Ha ocurrido un error al crear la comunidad. El nombre puede que ya esté siendo usado o el servidor no puede procesar la solicitud.",
     "Community ID: +<localpart />:%(domain)s": "ID de comunidad: +<localpart />:%(domain)s",
-    "Use this when referencing your community to others. The community ID cannot be changed.": "Use esto cuando haga referencia a su comunidad con otras. La identificación de la comunidad no se puede cambiar.",
+    "Use this when referencing your community to others. The community ID cannot be changed.": "Usa esto cuando hagas referencia a tu comunidad con otras. El ID de la comunidad no se puede cambiar.",
     "You can change this later if needed.": "Puede cambiar esto más tarde si es necesario.",
     "What's the name of your community or team?": "¿Cuál es el nombre de tu comunidad o equipo?",
-    "Enter name": "Ingrese su nombre",
+    "Enter name": "Introduce un nombre",
     "Add image (optional)": "Agregar imagen (opcional)",
     "An image will help people identify your community.": "Una imagen ayudará a las personas a identificar su comunidad.",
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Las salas privadas se pueden encontrar y unirse solo con invitación. Cualquier persona puede encontrar y unirse a las salas públicas.",
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Las salas privadas se pueden encontrar y unirse solo con invitación. Cualquier persona de esta comunidad puede encontrar salas públicas y unirse a ellas.",
-    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Puede activar esto si la sala solo se usará para colaborar con equipos internos en tu servidor base. Esto no se puede cambiar después.",
+    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Puedes activar esto si la sala solo se usará para colaborar con equipos internos en tu servidor base. No se puede cambiar después.",
     "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Puedes desactivar esto si la sala se utilizará para colaborar con equipos externos que tengan su propio servidor base. Esto no se puede cambiar después.",
     "Create a room in %(communityName)s": "Crea una sala en %(communityName)s",
-    "Block anyone not part of %(serverName)s from ever joining this room.": "Bloquea a cualquier persona que no sea parte de %(serverName)s para que no se una a esta sala.",
+    "Block anyone not part of %(serverName)s from ever joining this room.": "Evita que cualquier persona que no sea parte de %(serverName)s se una a esta sala.",
     "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Anteriormente usaste una versión más nueva de %(brand)s con esta sesión. Para volver a utilizar esta versión con cifrado de extremo a extremo, deberá cerrar sesión y volver a iniciar sesión.",
     "There was an error updating your community. The server is unable to process your request.": "Ha ocurrido un error al actualizar tu comunidad. El servidor no puede procesar la solicitud.",
     "Update community": "Actualizar comunidad",
     "To continue, use Single Sign On to prove your identity.": "Para continuar, utilice el inicio de sesión único para demostrar su identidad.",
     "Confirm to continue": "Confirmar para continuar",
-    "Click the button below to confirm your identity.": "Haga clic en el botón de abajo para confirmar su identidad.",
+    "Click the button below to confirm your identity.": "Haz clic en el botón de abajo para confirmar tu identidad.",
     "May include members not in %(communityName)s": "Puede incluir miembros que no están en %(communityName)s",
     "Start a conversation with someone using their name, username (like <userId/>) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>.": "Inicie una conversación con alguien usando su nombre, nombre de usuario (como<userId/>) o dirección de correo electrónico. Esto no los invitará a %(communityName)s Para invitar a alguien a %(communityName)s, haga clic <a>aquí</a>.",
     "You're all caught up.": "Estás al día.",
@@ -2247,7 +2247,7 @@
     "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Su servidor no responde a algunas de sus solicitudes. A continuación se presentan algunas de las razones más probables.",
     "The server (%(serverName)s) took too long to respond.": "El servidor (%(serverName)s) tardó demasiado en responder.",
     "Your firewall or anti-virus is blocking the request.": "Tu firewall o antivirus está bloqueando la solicitud.",
-    "A browser extension is preventing the request.": "Una extensión del navegador está impidiendo que se haga la solicitud.",
+    "A browser extension is preventing the request.": "Una extensión del navegador está impidiendo la solicitud.",
     "The server is offline.": "El servidor está desconectado.",
     "The server has denied your request.": "El servidor ha rechazado la solicitud.",
     "Your area is experiencing difficulties connecting to the internet.": "Su área está experimentando dificultades para conectarse a Internet.",
@@ -2269,7 +2269,7 @@
     "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use %(brand)s with an existing Matrix account on a different homeserver.": "Puede utilizar las opciones del servidor personalizado para iniciar sesión en otros servidores Matrix especificando una URL de servidor principal diferente. Esto le permite utilizar %(brand)s con una cuenta Matrix existente en un servidor doméstico diferente.",
     "Enter the location of your Element Matrix Services homeserver. It may use your own domain name or be a subdomain of <a>element.io</a>.": "Ingrese la ubicación de su servidor doméstico de Element Matrix Services. Puede usar su propio nombre de dominio o ser un subdominio de <a>element.io</a>.",
     "No files visible in this room": "No hay archivos visibles en esta sala",
-    "Attach files from chat or just drag and drop them anywhere in a room.": "Adjunte archivos desde el chat o simplemente arrástrelos y suéltelos en cualquier lugar de una sala.",
+    "Attach files from chat or just drag and drop them anywhere in a room.": "Adjunta archivos desde el chat o simplemente arrástralos y suéltalos en cualquier lugar de una sala.",
     "You’re all caught up": "Estás al día",
     "You have no visible notifications in this room.": "No tienes notificaciones visibles en esta sala.",
     "Delete the room address %(alias)s and remove %(name)s from the directory?": "¿Eliminar la dirección de la sala %(alias)s y eliminar %(name)s del directorio?",
@@ -2278,7 +2278,7 @@
     "Search rooms": "Buscar salas",
     "Create community": "Crear comunidad",
     "Failed to find the general chat for this community": "No se pudo encontrar el chat general de esta comunidad",
-    "Security & privacy": "Seguridad y Privacidad",
+    "Security & privacy": "Seguridad y privacidad",
     "All settings": "Todos los ajustes",
     "Feedback": "Realimentación",
     "Community settings": "Configuración de la comunidad",
@@ -2310,7 +2310,7 @@
     "%(brand)s iOS": "%(brand)s iOS",
     "%(brand)s Android": "%(brand)s Android",
     "or another cross-signing capable Matrix client": "u otro cliente Matrix con capacidad de firma cruzada",
-    "Without completing security on this session, it won’t have access to encrypted messages.": "Sin completar la seguridad en esta sesión, no tendrá acceso a los mensajes cifrados.",
+    "Without completing security on this session, it won’t have access to encrypted messages.": "Al no completar la seguridad en esta sesión, no tendrás acceso a los mensajes cifrados.",
     "Failed to re-authenticate due to a homeserver problem": "No ha sido posible volver a autenticarse debido a un problema con el servidor base",
     "Failed to re-authenticate": "No se pudo volver a autenticar",
     "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Recupere el acceso a su cuenta y recupere las claves de cifrado almacenadas en esta sesión. Sin ellos, no podrá leer todos sus mensajes seguros en ninguna sesión.",
@@ -2329,7 +2329,7 @@
     "Room Autocomplete": "Autocompletar sala",
     "User Autocomplete": "Autocompletar de usuario",
     "Confirm encryption setup": "Confirmar la configuración de cifrado",
-    "Click the button below to confirm setting up encryption.": "Haga clic en el botón de abajo para confirmar la configuración del cifrado.",
+    "Click the button below to confirm setting up encryption.": "Haz clic en el botón de abajo para confirmar la configuración del cifrado.",
     "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Protéjase contra la pérdida de acceso a los mensajes y datos cifrados haciendo una copia de seguridad de las claves de cifrado en su servidor.",
     "Generate a Security Key": "Generar una llave de seguridad",
     "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Generaremos una llave de seguridad para que la guardes en un lugar seguro, como un administrador de contraseñas o una caja fuerte.",
@@ -2752,7 +2752,7 @@
     "Active Widgets": "Widgets activos",
     "This version of %(brand)s does not support viewing some encrypted files": "Esta versión de %(brand)s no permite ver algunos archivos cifrados",
     "Use the <a>Desktop app</a> to search encrypted messages": "Usa la <a>aplicación de ordenador</a> para buscar en los mensajes cifrados",
-    "Use the <a>Desktop app</a> to see all encrypted files": "Usar la <a>aplicación de ordenador</a> para ver todos los archivos cifrados",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Usa la <a>aplicación de ordenador</a> para ver todos los archivos cifrados",
     "Video conference started by %(senderName)s": "Videoconferencia iniciada por %(senderName)s",
     "Video conference updated by %(senderName)s": "Videoconferencia actualizada por %(senderName)s",
     "You held the call <a>Resume</a>": "Has puesto la llamada en espera <a>Recuperar</a>",
@@ -2943,5 +2943,88 @@
     "Answered Elsewhere": "Respondida en otra parte",
     "The call could not be established": "No se ha podido establecer la llamada",
     "The other party declined the call.": "La otra persona ha rechazado la llamada.",
-    "Call Declined": "Llamada rechazada"
+    "Call Declined": "Llamada rechazada",
+    "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Tu clave de seguridad es una red de seguridad. Puedes usarla para volver a tener acceso a tus mensajes cifrados si te olvidas de tu frase de seguridad.",
+    "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Almacenaremos una copia cifrada de tus claves en nuestros servidores. Asegura tu copia de seguridad con una frase de seguridad.",
+    "<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 tu página de comunidad</h1>\n<p>\n\tUsa la descripción extendida para presentar la comunidad a nuevos participantes, o difunde\n\t<a href=\"foo\">enlaces</a> importantes\n</p>\n<p>\n\tPuedes incluso añadir imágenes con direcciones URL de Matrix <img src=\"mxc://url\" />\n</p>\n",
+    "If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Si te has olvidado de tu clave de seguridad puedes <button>configurar nuevos métodos de recuperación</button>",
+    "Access your secure message history and set up secure messaging by entering your Security Key.": "Accede a tu historial de mensajes seguros y configúralos introduciendo tu clave de seguridad.",
+    "If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Si has olvidado tu frase de seguridad puedes usar tu <button1>clave de seguridad</button1> o <button2>configurar nuevos métodos de recuperación</button2>",
+    "Access your secure message history and set up secure messaging by entering your Security Phrase.": "Accede a tu historia de mensajes seguros o configúralos escribiendo tu frase de seguridad.",
+    "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "No se ha podido descifrar la copia de seguridad con esa frase. Por favor, comprueba que hayas escrito bien la frase de seguridad.",
+    "Set up with a Security Key": "Configurar una clave de seguridad",
+    "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "La copia de seguridad no se ha podido descifrar con esta clave: por favor, comprueba que la que has introducido es correcta.",
+    "Security Key mismatch": "Las claves de seguridad no coinciden",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invita a alguien usando su nombre, nombre de usuario (ej.: <userId/>) o <a>compartiendo esta sala</a>.",
+    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "En esta conversación no hay nadie más, hasta que uno de los dos invite a alguien.",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "¿Seguro que quieres cancelar la creación del host? El proceso no podrá continuarse.",
+    "Use email or phone to optionally be discoverable by existing contacts.": "Usa tu correo electrónico o teléfono para que, opcionalmente, tus contactos puedan descubrir tu cuenta.",
+    "Enter Security Key": "Introduce la clave de seguridad",
+    "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "No se ha podido acceder al almacenamiento seguro. Por favor, comprueba que la frase de seguridad es correcta.",
+    "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>.": "Ten en cuenta que, si no añades un correo electrónico y olvidas tu contraseña, podrías <b>perder accceso para siempre a tu cuenta</b>.",
+    "We recommend you change your password and Security Key in Settings immediately": "Te recomendamos que cambies tu contraseña y clave de seguridad en ajustes inmediatamente",
+    "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invitar a alguien usando su nombre, dirección de correo, nombre de usuario (ej.: <userId/>) o <a>compartiendo esta sala</a>.",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Esto no les invitará a %(communityName)s. Para invitar a alguien a %(communityName)s, haz clic <a>aquí</a>",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Al continuar con el proceso de configuración, %(hostSignupBrand)s podrá acceder a tu cuenta para comprobar las direcciones de correo verificadas. Los datos no se almacenan.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "No se ha podido conectar con tu servidor base. Por favor, cierra este mensaje e inténtalo de nuevo.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Guardar mensajes cifrados de forma segura y local para que aparezcan en los resultados de búsqueda, usando %(size)s para almacenar mensajes 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": "Guardar mensajes cifrados de forma segura y local para que aparezcan en los resultados de búsqueda, usando %(size)s para almacenar mensajes de %(rooms)s salas.",
+    "Offline encrypted messaging using dehydrated devices": "Mensajería cifrada y offline usando dispositivos deshidratados",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "Enviar mensajes de tipo <b>%(msgtype)s</b> en tu nombre a tu sala activa",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Enviar mensajes de tipo <b>%(msgtype)s</b> en tu nombre a esta sala",
+    "See general files posted to your active room": "Ver archivos enviados a tu sala activa",
+    "See general files posted to this room": "Ver archivos enviados a esta sala",
+    "Send general files as you in your active room": "Enviar archivos en tu nombre a tu sala activa",
+    "See videos posted to your active room": "Ver los vídeos publicados a tu sala activa",
+    "Send videos as you in your active room": "Enviar vídeos en tu nombre a tu sala activa",
+    "See images posted to your active room": "Ver las imágenes enviadas a tu sala activa",
+    "Send images as you in your active room": "Enviar imágenes en tu nombre a tu sala activa",
+    "See emotes posted to your active room": "Ver las reacciones publicadas en tu sala activa",
+    "Send emotes as you in your active room": "Reaccionar en tu nombre a tu sala activa",
+    "See <b>%(eventType)s</b> events posted to your active room": "Ver los eventos de tipo <b>%(eventType)s</b> publicados en tu sala activa",
+    "Send stickers to your active room as you": "Enviar etiquetas a tu sala activa en tu nombre",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Empieza una conversación con alguien usando su nombre o nombre de usuario (como <userId/>).",
+    "Start a conversation with someone using their name, email address or username (like <userId/>).": "Empieza una conversación con alguien usando su nombre, correo electrónico o nombre de usuario (como <userId/>).",
+    "Minimize dialog": "Minimizar",
+    "Maximize dialog": "Maximizar",
+    "See <b>%(msgtype)s</b> messages posted to your active room": "Ver mensajes de tipo <b>%(msgtype)s</b> enviados a tu sala activa",
+    "See <b>%(msgtype)s</b> messages posted to this room": "Ver mensajes de tipo <b>%(msgtype)s</b> enviados a esta sala",
+    "Show chat effects (animations when receiving e.g. confetti)": "Mostrar efectos de chat (animaciones al recibir ciertos mensajes, como confeti)",
+    "Expand code blocks by default": "Expandir bloques de ćodigo por defecto",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Más información en nuestra <privacyPolicyLink />, <termsOfServiceLink /> y <cookiePolicyLink />.",
+    "Cookie Policy": "Política de cookies",
+    "Privacy Policy": "Política de privacidad",
+    "%(hostSignupBrand)s Setup": "Configuración de %(hostSignupBrand)s",
+    "You should know": "Conviene saber",
+    "Upgrade to %(hostSignupBrand)s": "Contratar %(hostSignupBrand)s",
+    "Show line numbers in code blocks": "Mostrar números de línea en bloques de ćodigo",
+    "This is the beginning of your direct message history with <displayName/>.": "Este es el inicio de tu historial de mensajes directos con <displayName/>.",
+    "Recently visited rooms": "Salas visitadas recientemente",
+    "Abort": "Cancelar",
+    "Confirm abort of host creation": "Confirma que quieres cancelar la creación del host",
+    "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "CONSEJO: Si creas una incidencia, adjunta <debugLogsLink>tus registros de depuración</debugLogsLink> para ayudarnos a localizar el problema.",
+    "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Por favor, echa un vistazo a <existingIssuesLink>las incidenias de Github</existingIssuesLink> primero. Si no encuentras nada relacionado, <newIssueLink>crea una nueva</newIssueLink>.",
+    "Edit Values": "Editar valores",
+    "Values at explicit levels in this room:": "Valores a niveles explícitos en esta sala:",
+    "Values at explicit levels:": "Valores a niveles explícitos:",
+    "Value in this room:": "Valor en esta sala:",
+    "Value:": "Valor:",
+    "Save setting values": "Guardar valores de ajustes",
+    "Values at explicit levels in this room": "Valores a niveles explícitos en esta sala",
+    "Values at explicit levels": "Valores a niveles explícitos",
+    "Settable at room": "Establecible a nivel de sala",
+    "Settable at global": "Establecible globalmente",
+    "Level": "Nivel",
+    "Setting definition:": "Definición del ajuste:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Esta interfaz NO comprueba los tipos de dato de los valores. Usar bajo tu responsabilidad.",
+    "Caution:": "Precaución:",
+    "Setting:": "Ajuste:",
+    "Value in this room": "Valor en esta sala",
+    "Value": "Valor",
+    "Setting ID": "ID de ajuste",
+    "Failed to save settings": "No se han podido guardar los ajustes",
+    "Settings Explorer": "Explorador de ajustes",
+    "Windows": "Ventanas",
+    "Share your screen": "Compartir pantalla",
+    "Screens": "Pantallas"
 }

From 9d65f0ca41ff0431301036d2c4e41e3ac9fdf6f7 Mon Sep 17 00:00:00 2001
From: Thibault Martin <mail@thibaultmart.in>
Date: Fri, 5 Mar 2021 21:31:18 +0000
Subject: [PATCH 302/389] Translated using Weblate (French)

Currently translated at 100.0% (2780 of 2780 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 | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index 3d024f6e18..e170ec1414 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -3043,5 +3043,27 @@
     "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Votre serveur d’accueil n’est pas accessible, nous n’avons pas pu vous connecter. Merci de réessayer. Si cela persiste, merci de contacter l’administrateur de votre serveur d’accueil.",
     "Try again": "Réessayez",
     "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Nous avons demandé à votre navigateur de mémoriser votre serveur d’accueil, mais il semble l’avoir oublié. Rendez-vous à la page de connexion et réessayez.",
-    "We couldn't log you in": "Impossible de vous déconnecter"
+    "We couldn't log you in": "Impossible de vous déconnecter",
+    "Upgrade to %(hostSignupBrand)s": "Mettre à jour vers %(hostSignupBrand)s",
+    "Edit Values": "Modifier les valeurs",
+    "Values at explicit levels in this room:": "Valeurs pour les rangs explicites de ce salon :",
+    "Values at explicit levels:": "Valeurs pour les rangs explicites :",
+    "Value in this room:": "Valeur pour ce salon :",
+    "Value:": "Valeur :",
+    "Save setting values": "Enregistrer les valeurs des paramètres",
+    "Values at explicit levels in this room": "Valeurs pour des rangs explicites dans ce salon",
+    "Values at explicit levels": "Valeurs pour des rangs explicites",
+    "Settable at room": "Définissable par salon",
+    "Settable at global": "Définissable de manière globale",
+    "Level": "Rang",
+    "Setting definition:": "Définition du paramètre :",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Cette interface ne vérifie pas les types des valeurs. Utilisez la à vos propres risques.",
+    "Caution:": "Attention :",
+    "Setting:": "Paramètre :",
+    "Value in this room": "Valeur pour ce salon",
+    "Value": "Valeur",
+    "Setting ID": "Identifiant de paramètre",
+    "Failed to save settings": "Échec lors de la sauvegarde des paramètres",
+    "Settings Explorer": "Explorateur de paramètres",
+    "Show chat effects (animations when receiving e.g. confetti)": "Afficher les animations de conversation (animations lors de la réception par ex. de confettis)"
 }

From 49c40e662ce40ba2f967708e07e92579a37de7e6 Mon Sep 17 00:00:00 2001
From: Roel ter Maat <roel.termaat@nedap.com>
Date: Tue, 2 Mar 2021 21:10:34 +0000
Subject: [PATCH 303/389] Translated using Weblate (Dutch)

Currently translated at 100.0% (2780 of 2780 strings)

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

diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 0d80e520c8..db3255c051 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -165,7 +165,7 @@
     "Failed to set display name": "Instellen van weergavenaam is mislukt",
     "Failed to unban": "Ontbannen mislukt",
     "Failed to upload profile picture!": "Uploaden van profielfoto is mislukt!",
-    "Failed to verify email address: make sure you clicked the link in the email": "Kan het e-mailadres niet verifiëren: zorg ervoor dat u de koppeling in de e-mail heeft aangeklikt",
+    "Failed to verify email address: make sure you clicked the link in the email": "Kan het e-mailadres niet verifiëren: zorg ervoor dat je de koppeling in de e-mail hebt aangeklikt",
     "Failure to create room": "Aanmaken van gesprek is mislukt",
     "Favourites": "Favorieten",
     "Fill screen": "Scherm vullen",
@@ -240,7 +240,7 @@
     "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s heeft %(targetDisplayName)s in het gesprek uitgenodigd.",
     "Server error": "Serverfout",
     "Server may be unavailable, overloaded, or search timed out :(": "De server is misschien onbereikbaar of overbelast, of het zoeken duurde te lang :(",
-    "Server may be unavailable, overloaded, or you hit a bug.": "De server is misschien onbereikbaar of overbelast, of u bent een fout tegengekomen.",
+    "Server may be unavailable, overloaded, or you hit a bug.": "De server is misschien onbereikbaar of overbelast, of je bent een fout tegengekomen.",
     "Server unavailable, overloaded, or something else went wrong.": "De server is onbereikbaar of overbelast, of er is iets anders foutgegaan.",
     "Session ID": "Sessie-ID",
     "%(senderName)s kicked %(targetName)s.": "%(senderName)s heeft %(targetName)s het gesprek uitgestuurd.",
@@ -304,8 +304,8 @@
     "Who can read history?": "Wie kan de geschiedenis lezen?",
     "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s heeft de uitnodiging van %(targetName)s ingetrokken.",
     "You are already in a call.": "U bent al in gesprek.",
-    "You cannot place a call with yourself.": "U kunt uzelf niet bellen.",
-    "You cannot place VoIP calls in this browser.": "U kunt in deze browser geen VoIP-oproepen plegen.",
+    "You cannot place a call with yourself.": "Je kunt jezelf niet bellen.",
+    "You cannot place VoIP calls in this browser.": "Je kunt in deze browser geen VoIP-oproepen plegen.",
     "You do not have permission to post to this room": "U heeft geen toestemming actief aan dit gesprek deel te nemen",
     "You have <a>disabled</a> URL previews by default.": "U heeft URL-voorvertoningen standaard <a>uitgeschakeld</a>.",
     "You have <a>enabled</a> URL previews by default.": "U heeft URL-voorvertoningen standaard <a>ingeschakeld</a>.",
@@ -420,7 +420,7 @@
     "Which rooms would you like to add to this community?": "Welke gesprekken wilt u toevoegen aan deze gemeenschap?",
     "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Widgets verwijderen geldt voor alle deelnemers aan dit gesprek. Weet u zeker dat u deze widget wilt verwijderen?",
     "Delete Widget": "Widget verwijderen",
-    "Who would you like to add to this community?": "Wie wilt u toevoegen aan deze gemeenschap?",
+    "Who would you like to add to this community?": "Wie wil je toevoegen aan deze gemeenschap?",
     "Invite to Community": "Uitnodigen tot gemeenschap",
     "Show these rooms to non-members on the community page and room list?": "Deze gesprekken tonen aan niet-leden op de gemeenschapspagina en gesprekslijst?",
     "Add rooms to the community": "Voeg gesprekken toe aan de gemeenschap",
@@ -628,12 +628,12 @@
     "Room Notification": "Groepsgespreksmelding",
     "The information being sent to us to help make %(brand)s better includes:": "De informatie die naar ons wordt verstuurd om %(brand)s te verbeteren bevat:",
     "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Waar deze pagina identificeerbare informatie bevat, zoals een gespreks-, gebruikers- of groeps-ID, zullen deze gegevens verwijderd worden voordat ze naar de server gestuurd worden.",
-    "The platform you're on": "Het platform dat u gebruikt",
+    "The platform you're on": "Het platform dat je gebruikt",
     "The version of %(brand)s": "De versie van %(brand)s",
-    "Your language of choice": "De door u gekozen taal",
-    "Which officially provided instance you are using, if any": "Welke officieel aangeboden instantie u eventueel gebruikt",
-    "Whether or not you're using the Richtext mode of the Rich Text Editor": "Of u de tekstverwerker al dan niet in de modus voor opgemaakte tekst gebruikt",
-    "Your homeserver's URL": "De URL van uw homeserver",
+    "Your language of choice": "De door jou gekozen taal",
+    "Which officially provided instance you are using, if any": "Welke officieel aangeboden instantie je eventueel gebruikt",
+    "Whether or not you're using the Richtext mode of the Rich Text Editor": "Of je de tekstverwerker al dan niet in de modus voor opgemaakte tekst gebruikt",
+    "Your homeserver's URL": "De URL van je homeserver",
     "<a>In reply to</a> <pill>": "<a>Als antwoord op</a> <pill>",
     "This room is not public. You will not be able to rejoin without an invite.": "Dit is geen openbaar gesprek. Slechts op uitnodiging zult u opnieuw kunnen toetreden.",
     "were unbanned %(count)s times|one": "zijn ontbannen",
@@ -783,9 +783,9 @@
     "Failed to send logs: ": "Versturen van logboeken mislukt: ",
     "Preparing to send logs": "Logboeken worden voorbereid voor versturen",
     "e.g. %(exampleValue)s": "bv. %(exampleValue)s",
-    "Every page you use in the app": "Iedere bladzijde die u in de toepassing gebruikt",
+    "Every page you use in the app": "Iedere bladzijde die je in de toepassing gebruikt",
     "e.g. <CurrentPageURL>": "bv. <CurrentPageURL>",
-    "Your device resolution": "De resolutie van uw apparaat",
+    "Your device resolution": "De resolutie van je apparaat",
     "Missing roomId.": "roomId ontbreekt.",
     "Always show encryption icons": "Versleutelingspictogrammen altijd tonen",
     "Send analytics data": "Statistische gegevens versturen",
@@ -813,7 +813,7 @@
     "A call is currently being placed!": "Er wordt al een oproep gemaakt!",
     "A call is already in progress!": "Er is al een gesprek actief!",
     "Permission Required": "Toestemming vereist",
-    "You do not have permission to start a conference call in this room": "U heeft geen toestemming in dit groepsgesprek een vergadergesprek te starten",
+    "You do not have permission to start a conference call in this room": "Je hebt geen toestemming in dit groepsgesprek een vergadergesprek te starten",
     "This event could not be displayed": "Deze gebeurtenis kon niet weergegeven worden",
     "Demote yourself?": "Uzelf degraderen?",
     "Demote": "Degraderen",
@@ -842,9 +842,9 @@
     "Bulk options": "Bulkopties",
     "This homeserver has hit its Monthly Active User limit.": "Deze homeserver heeft zijn limiet voor maandelijks actieve gebruikers bereikt.",
     "This homeserver has exceeded one of its resource limits.": "Deze homeserver heeft één van zijn systeembronlimieten overschreden.",
-    "Whether or not you're logged in (we don't record your username)": "Of u al dan niet ingelogd bent (we slaan uw gebruikersnaam niet op)",
+    "Whether or not you're logged in (we don't record your username)": "Of je al dan niet ingelogd bent (we slaan je gebruikersnaam niet op)",
     "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Het bestand ‘%(fileName)s’ is groter dan de uploadlimiet van de homeserver",
-    "Unable to load! Check your network connectivity and try again.": "Laden mislukt! Controleer uw netwerktoegang en probeer het nogmaals.",
+    "Unable to load! Check your network connectivity and try again.": "Laden mislukt! Controleer je netwerktoegang en probeer het nogmaals.",
     "Failed to invite users to the room:": "Kon de volgende gebruikers hier niet uitnodigen:",
     "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Plakt ¯\\_(ツ)_/¯ vóór een bericht zonder opmaak",
     "Upgrades a room to a new version": "Actualiseert het gesprek tot een nieuwe versie",
@@ -1255,9 +1255,9 @@
     "The homeserver may be unavailable or overloaded.": "De homeserver is mogelijk onbereikbaar of overbelast.",
     "You have %(count)s unread notifications in a prior version of this room.|other": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.",
     "You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.",
-    "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u de icoontjes voor recente gesprekken (boven de gesprekkenlijst) al dan niet gebruikt",
+    "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of je de icoontjes voor recente gesprekken (boven de gesprekkenlijst) al dan niet gebruikt",
     "Replying With Files": "Beantwoorden met bestanden",
-    "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Het is momenteel niet mogelijk met een bestand te antwoorden. Wilt u dit bestand uploaden zonder te antwoorden?",
+    "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Het is momenteel niet mogelijk met een bestand te antwoorden. Wil je dit bestand uploaden zonder te antwoorden?",
     "The file '%(fileName)s' failed to upload.": "Het bestand ‘%(fileName)s’ kon niet geüpload worden.",
     "Rotate counter-clockwise": "Tegen de klok in draaien",
     "Rotate clockwise": "Met de klok mee draaien",
@@ -1433,8 +1433,8 @@
     "Command Help": "Hulp bij opdrachten",
     "No identity server is configured: add one in server settings to reset your password.": "Er is geen identiteitsserver geconfigureerd: voeg er één toe in de serverinstellingen om uw wachtwoord opnieuw in te stellen.",
     "Call failed due to misconfigured server": "Oproep mislukt door verkeerd geconfigureerde server",
-    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Vraag uw homeserverbeheerder (<code>%(homeserverDomain)s</code>) een TURN-server te configureren voor de betrouwbaarheid van de oproepen.",
-    "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.": "U kunt ook de publieke server op <code>turn.matrix.org</code> gebruiken, maar dit zal minder betrouwbaar zijn, en zal uw IP-adres met die server delen. U kunt dit ook beheren in de Instellingen.",
+    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Vraag je homeserverbeheerder (<code>%(homeserverDomain)s</code>) een TURN-server te configureren voor de betrouwbaarheid van de oproepen.",
+    "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.": "Je kunt ook de publieke server op <code>turn.matrix.org</code> gebruiken, maar dit zal minder betrouwbaar zijn, en zal uw IP-adres met die server delen. Je kunt dit ook beheren in de Instellingen.",
     "Try using turn.matrix.org": "Probeer turn.matrix.org te gebruiken",
     "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Sta de terugvalserver voor oproepbijstand turn.matrix.org toe wanneer uw homeserver er geen aanbiedt (uw IP-adres wordt gedeeld gedurende een oproep)",
     "Identity server has no terms of service": "De identiteitsserver heeft geen dienstvoorwaarden",
@@ -1636,9 +1636,9 @@
     "Unable to load session list": "Kan sessielijst niet laden",
     "Delete %(count)s sessions|other": "%(count)s sessies verwijderen",
     "Delete %(count)s sessions|one": "%(count)s sessie verwijderen",
-    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Of u %(brand)s op een apparaat gebruikt waarop een aanraakscherm de voornaamste invoermethode is",
-    "Whether you're using %(brand)s as an installed Progressive Web App": "Of u %(brand)s gebruikt als een geïnstalleerde Progressive-Web-App",
-    "Your user agent": "Uw gebruikersagent",
+    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Of je %(brand)s op een apparaat gebruikt waarop een aanraakscherm de voornaamste invoermethode is",
+    "Whether you're using %(brand)s as an installed Progressive Web App": "Of je %(brand)s gebruikt als een geïnstalleerde Progressive-Web-App",
+    "Your user agent": "Jouw gebruikersagent",
     "If you cancel now, you won't complete verifying the other user.": "Als u nu annuleert zult u de andere gebruiker niet verifiëren.",
     "If you cancel now, you won't complete verifying your other session.": "Als u nu annuleert zult u uw andere sessie niet verifiëren.",
     "Cancel entering passphrase?": "Wachtwoordinvoer annuleren?",
@@ -1973,11 +1973,11 @@
     "Where you’re logged in": "Waar u ingelogd bent",
     "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Beheer hieronder de namen van uw sessies en meld ze af. <a>Of verifieer ze in uw gebruikersprofiel</a>.",
     "Use Single Sign On to continue": "Ga verder met eenmalige aanmelding",
-    "Confirm adding this email address by using Single Sign On to prove your identity.": "Bevestig uw identiteit met uw eenmalige aanmelding om dit e-mailadres toe te voegen.",
+    "Confirm adding this email address by using Single Sign On to prove your identity.": "Bevestig je identiteit met je eenmalige aanmelding om dit e-mailadres toe te voegen.",
     "Single Sign On": "Eenmalige aanmelding",
     "Confirm adding email": "Bevestig toevoegen van het e-mailadres",
     "Click the button below to confirm adding this email address.": "Klik op de knop hieronder om dit e-mailadres toe te voegen.",
-    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bevestig uw identiteit met uw eenmalige aanmelding om dit telefoonnummer toe te voegen.",
+    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bevestig je identiteit met je eenmalige aanmelding om dit telefoonnummer toe te voegen.",
     "Confirm adding phone number": "Bevestig toevoegen van het telefoonnummer",
     "Click the button below to confirm adding this phone number.": "Klik op de knop hieronder om het toevoegen van dit telefoonnummer te bevestigen.",
     "If you cancel now, you won't complete your operation.": "Als u de operatie afbreekt kunt u haar niet voltooien.",

From 592bd427a5995fe61b24b9ea40c1931c55eef015 Mon Sep 17 00:00:00 2001
From: Ihor Hordiichuk <igor_ck@outlook.com>
Date: Mon, 1 Mar 2021 21:11:40 +0000
Subject: [PATCH 304/389] Translated using Weblate (Ukrainian)

Currently translated at 53.6% (1492 of 2780 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 | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index adb830c123..5f392295c3 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -325,7 +325,7 @@
     "Displays action": "Відбиває дію",
     "Reason": "Причина",
     "%(senderName)s requested a VoIP conference.": "%(senderName)s бажає розпочати дзвінок-конференцію.",
-    "%(senderName)s invited %(targetName)s.": "%(senderName)s запросив/ла %(targetName)s.",
+    "%(senderName)s invited %(targetName)s.": "%(senderName)s запрошує %(targetName)s.",
     "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s змінює своє видиме ім'я на %(displayName)s.",
     "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s зазначив(-ла) своє видиме ім'я: %(displayName)s.",
     "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s видалив(-ла) своє видиме ім'я (%(oldDisplayName)s).",
@@ -1164,7 +1164,7 @@
     "Show hidden events in timeline": "Показувати приховані події у часоряді",
     "Show previews/thumbnails for images": "Показувати попередній перегляд зображень",
     "Compare a unique set of emoji if you don't have a camera on either device": "Порівняйте унікальну низку емодзі якщо ви не маєте камери на жодному пристрої",
-    "Confirm the emoji below are displayed on both sessions, in the same order:": "Підтвердьте, що нижчевказані емодзі відбиваються в обох сеансах в однаковому порядку:",
+    "Confirm the emoji below are displayed on both sessions, in the same order:": "Підтвердьте, що емодзі внизу показано в обох сеансах в однаковому порядку:",
     "Verify this user by confirming the following emoji appear on their screen.": "Звірте цього користувача підтвердженням того, що наступні емодзі з'являються на його екрані.",
     "Emoji picker": "Обирач емодзі",
     "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "Сеанс, який ви намагаєтесь звірити, не підтримує сканування QR-коду або звіряння за допомогою емодзі, що є підтримувані %(brand)s. Спробуйте використати інший клієнт.",
@@ -1597,5 +1597,8 @@
     "Enable encryption?": "Увімкнути шифрування?",
     "Enable room encryption": "Увімкнути шифрування кімнати",
     "Encryption": "Шифрування",
-    "Try again": "Спробувати ще раз"
+    "Try again": "Спробувати ще раз",
+    "%(creator)s created this DM.": "%(creator)s створює цю приватну розмову.",
+    "Share Link to User": "Поділитися посиланням на користувача",
+    "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Повідомлення тут захищено наскрізним шифруванням. Підтвердьте %(displayName)s у їхньому профілі — натиснувши на їх аватар."
 }

From ca729d35aa90eb55b908dd4101768f44d7589701 Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Tue, 2 Mar 2021 12:00:40 +0000
Subject: [PATCH 305/389] Translated using Weblate (Italian)

Currently translated at 100.0% (2780 of 2780 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 | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 2764863e77..3e5ad9296b 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -3065,5 +3065,27 @@
     "Recently visited rooms": "Stanze visitate di recente",
     "Show line numbers in code blocks": "Mostra numeri di riga nei blocchi di codice",
     "Expand code blocks by default": "Espandi blocchi di codice in modo predefinito",
-    "Show stickers button": "Mostra pulsante adesivi"
+    "Show stickers button": "Mostra pulsante adesivi",
+    "Upgrade to %(hostSignupBrand)s": "Aggiorna a %(hostSignupBrand)s",
+    "Edit Values": "Modifica valori",
+    "Values at explicit levels in this room:": "Valori a livelli espliciti in questa stanza:",
+    "Values at explicit levels:": "Valori a livelli espliciti:",
+    "Value in this room:": "Valore in questa stanza:",
+    "Value:": "Valore:",
+    "Save setting values": "Salva valori impostazione",
+    "Values at explicit levels in this room": "Valori a livelli espliciti in questa stanza",
+    "Values at explicit levels": "Valori a livelli espliciti",
+    "Settable at room": "Impostabile per stanza",
+    "Settable at global": "Impostabile globalmente",
+    "Level": "Livello",
+    "Setting definition:": "Definizione impostazione:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Questa interfaccia NON controlla i tipi dei valori. Usa a tuo rischio.",
+    "Caution:": "Attenzione:",
+    "Setting:": "Impostazione:",
+    "Value in this room": "Valore in questa stanza",
+    "Value": "Valore",
+    "Setting ID": "ID impostazione",
+    "Failed to save settings": "Impossibile salvare le impostazioni",
+    "Settings Explorer": "Esploratore di impostazioni",
+    "Show chat effects (animations when receiving e.g. confetti)": "Mostra effetti chat (animazioni quando si ricevono ad es. coriandoli)"
 }

From 6b098a2bafbf49acea769a17be976370c067cfbd Mon Sep 17 00:00:00 2001
From: Kaede <contact+element_translations@kaede.ch>
Date: Tue, 2 Mar 2021 06:04:12 +0000
Subject: [PATCH 306/389] Translated using Weblate (Japanese)

Currently translated at 50.2% (1398 of 2780 strings)

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

diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index bab9d94935..b5e28a9c1e 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -416,8 +416,8 @@
     "Share Link to User": "ユーザーへのリンクを共有する",
     "Unmute": "ミュート解除",
     "Admin Tools": "管理者ツール",
-    "and %(count)s others...|other": "そして、他 %(count)s ...",
-    "and %(count)s others...|one": "そして、もう1つ...",
+    "and %(count)s others...|other": "他 %(count)s 人...",
+    "and %(count)s others...|one": "他1人...",
     "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (権限レベル: %(powerLevelNumber)s )",
     "Attachment": "付属品",
     "Hangup": "電話を切る",
@@ -625,7 +625,7 @@
     "Custom level": "カスタムレベル",
     "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "返信されたイベントを読み込めません。存在しないか、表示する権限がありません。",
     "<a>In reply to</a> <pill>": "<a>返信</a> <pill>",
-    "And %(count)s more...|other": "そして %(count)s もっと...",
+    "And %(count)s more...|other": "他 %(count)s 人以上...",
     "ex. @bob:example.com": "例 @bob:example.com",
     "Add User": "ユーザーを追加",
     "Matrix ID": "Matirx ID",

From d3af6b840a5993d3684e190c6d478fb6218d144c Mon Sep 17 00:00:00 2001
From: Rintan <rintanbroadleaf@outlook.jp>
Date: Mon, 1 Mar 2021 04:42:56 +0000
Subject: [PATCH 307/389] Translated using Weblate (Japanese)

Currently translated at 50.2% (1398 of 2780 strings)

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

diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index b5e28a9c1e..ea7d51ae05 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -1533,5 +1533,7 @@
     "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "あなたの他のセッションに<requestLink>暗号鍵を再リクエストする</requestLink>。",
     "Block anyone not part of %(serverName)s from ever joining this room.": "%(serverName)s 以外からの参加をブロック",
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "プライベートな部屋は招待者のみが参加できます。公開された部屋は誰でも検索・参加できます。",
-    "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Matrix 関連のセキュリティ問題を報告するには、Matrix.org の <a>Security Disclosure Policy</a> をご覧ください。"
+    "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Matrix 関連のセキュリティ問題を報告するには、Matrix.org の <a>Security Disclosure Policy</a> をご覧ください。",
+    "Confirm adding email": "メールアドレスの追加を確認する",
+    "Confirm adding this email address by using Single Sign On to prove your identity.": "シングルサインオンを使用して本人確認を行い、メールアドレスの追加を承認してください。"
 }

From a82ac0cc90f427988d236f4065e13716adf34b97 Mon Sep 17 00:00:00 2001
From: waclaw66 <waclaw66@seznam.cz>
Date: Wed, 3 Mar 2021 09:51:41 +0000
Subject: [PATCH 308/389] Translated using Weblate (Czech)

Currently translated at 100.0% (2780 of 2780 strings)

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

diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index 47a99ab670..db01caed97 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -490,7 +490,7 @@
     "were invited %(count)s times|other": "byli %(count)s krát pozváni",
     "were invited %(count)s times|one": "byli pozváni",
     "was invited %(count)s times|other": "byl %(count)s krát pozván",
-    "was invited %(count)s times|one": "byl pozván",
+    "was invited %(count)s times|one": "byl(a) pozván(a)",
     "were banned %(count)s times|other": "mělid %(count)s krát zakázaný vstup",
     "were banned %(count)s times|one": "měli zakázaný vstup",
     "was banned %(count)s times|other": "měl %(count)s krát zakázaný vstup",

From b71695d803d2c80998e7597e8e17c8c538ff4b3d Mon Sep 17 00:00:00 2001
From: MamasLT <admin@eastwesthost.com>
Date: Wed, 3 Mar 2021 20:57:29 +0000
Subject: [PATCH 309/389] Translated using Weblate (Lithuanian)

Currently translated at 68.5% (1905 of 2780 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 | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json
index 7edb8f0c55..80b70e67e4 100644
--- a/src/i18n/strings/lt.json
+++ b/src/i18n/strings/lt.json
@@ -429,7 +429,7 @@
     "Unban this user?": "Atblokuoti šį vartotoją?",
     "Ban this user?": "Užblokuoti šį vartotoją?",
     "Failed to ban user": "Nepavyko užblokuoti vartotojo",
-    "Invited": "Pakviestas",
+    "Invited": "Pakviesta",
     "Filter room members": "Filtruoti kambario dalyvius",
     "Server unavailable, overloaded, or something else went wrong.": "Serveris neprieinamas, perkrautas arba nutiko kažkas kito.",
     "%(duration)ss": "%(duration)s sek",

From 0f5f1132a5617c9dce893d4cc99e2750a48b0a97 Mon Sep 17 00:00:00 2001
From: HelaBasa <R45XvezA@protonmail.ch>
Date: Tue, 2 Mar 2021 17:15:57 +0000
Subject: [PATCH 310/389] Translated using Weblate (Sinhala)

Currently translated at 0.2% (7 of 2780 strings)

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

diff --git a/src/i18n/strings/si.json b/src/i18n/strings/si.json
index 11da7b7e4c..5a81da879f 100644
--- a/src/i18n/strings/si.json
+++ b/src/i18n/strings/si.json
@@ -4,5 +4,6 @@
     "Use Single Sign On to continue": "ඉදිරියට යාමට තනි පුරනය වීම භාවිතා කරන්න",
     "Confirm adding this email address by using Single Sign On to prove your identity.": "ඔබගේ අනන්‍යතාවය සනාථ කිරීම සඳහා තනි පුරනය භාවිතා කිරීමෙන් මෙම විද්‍යුත් තැපැල් ලිපිනය එක් කිරීම තහවුරු කරන්න.",
     "Confirm": "තහවුරු කරන්න",
-    "Add Email Address": "විද්‍යුත් තැපැල් ලිපිනය එක් කරන්න"
+    "Add Email Address": "විද්‍යුත් තැපැල් ලිපිනය එක් කරන්න",
+    "Sign In": "පිවිසෙන්න"
 }

From 1427aa4203a5610be22bdb43eaab391cf83d88cb Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 04:41:51 +0000
Subject: [PATCH 311/389] fix mx_EncryptionInfo_spinner padding in dialogs

---
 res/css/views/right_panel/_EncryptionInfo.scss | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/res/css/views/right_panel/_EncryptionInfo.scss b/res/css/views/right_panel/_EncryptionInfo.scss
index e13b1b6802..b3d4275f60 100644
--- a/res/css/views/right_panel/_EncryptionInfo.scss
+++ b/res/css/views/right_panel/_EncryptionInfo.scss
@@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-.mx_UserInfo {
-    .mx_EncryptionInfo_spinner {
-        .mx_Spinner {
-            margin-top: 25px;
-            margin-bottom: 15px;
-        }
-
-        text-align: center;
+.mx_EncryptionInfo_spinner {
+    .mx_Spinner {
+        margin-top: 25px;
+        margin-bottom: 15px;
     }
+
+    text-align: center;
 }

From 76738184003e1e2ad8d24ad2fbbab3b5dab2afc2 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 04:43:59 +0000
Subject: [PATCH 312/389] switch to using an explicit verify button when
 cross-signing a new login

---
 .../structures/auth/SetupEncryptionBody.js    | 46 +++++++++++--------
 1 file changed, 28 insertions(+), 18 deletions(-)

diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js
index 3e7264dfec..d2b3fbd3b9 100644
--- a/src/components/structures/auth/SetupEncryptionBody.js
+++ b/src/components/structures/auth/SetupEncryptionBody.js
@@ -19,6 +19,8 @@ import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import SdkConfig from '../../../SdkConfig';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import Modal from '../../../Modal';
+import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
 import * as sdk from '../../../index';
 import {
     SetupEncryptionStore,
@@ -81,6 +83,22 @@ export default class SetupEncryptionBody extends React.Component {
         store.usePassPhrase();
     }
 
+    _onVerifyClick = () => {
+        const cli = MatrixClientPeg.get();
+        const userId = cli.getUserId();
+        const requestPromise = cli.requestVerification(userId);
+
+        this.props.onFinished(true);
+        Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
+            verificationRequestPromise: requestPromise,
+            member: cli.getUser(userId),
+            onFinished: async () => {
+                const request = await requestPromise;
+                request.cancel();
+            },
+        });
+    }
+
     onSkipClick = () => {
         const store = SetupEncryptionStore.sharedInstance();
         store.skip();
@@ -132,32 +150,24 @@ export default class SetupEncryptionBody extends React.Component {
                 </AccessibleButton>;
             }
 
+            let verifyButton;
+            if (store.hasDevicesToVerifyAgainst) {
+                verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
+                    { _t("Verify against another session") }
+                </AccessibleButton>;
+            }
+
             const brand = SdkConfig.get().brand;
 
             return (
                 <div>
                     <p>{_t(
-                        "Confirm your identity by verifying this login from one of your other sessions, " +
-                        "granting it access to encrypted messages.",
+                        "Verify this login to access your encrypted messages and " +
+                        "prove to others that this login is really you."
                     )}</p>
-                    <p>{_t(
-                        "This requires the latest %(brand)s on your other devices:",
-                        { brand },
-                    )}</p>
-
-                    <div className="mx_CompleteSecurity_clients">
-                        <div className="mx_CompleteSecurity_clients_desktop">
-                            <div>{_t("%(brand)s Web", { brand })}</div>
-                            <div>{_t("%(brand)s Desktop", { brand })}</div>
-                        </div>
-                        <div className="mx_CompleteSecurity_clients_mobile">
-                            <div>{_t("%(brand)s iOS", { brand })}</div>
-                            <div>{_t("%(brand)s Android", { brand })}</div>
-                        </div>
-                        <p>{_t("or another cross-signing capable Matrix client")}</p>
-                    </div>
 
                     <div className="mx_CompleteSecurity_actionRow">
+                        {verifyButton}
                         {useRecoveryKeyButton}
                         <AccessibleButton kind="danger" onClick={this.onSkipClick}>
                             {_t("Skip")}

From 6df8157a40e6da23850ad5fd1a17294d6ba23382 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 04:45:44 +0000
Subject: [PATCH 313/389] cancel VRD at the Modal level, so clicking in the bg
 cancels it

---
 .../views/dialogs/NewSessionReviewDialog.js         |  4 ++++
 .../views/dialogs/VerificationRequestDialog.js      | 13 ++-----------
 2 files changed, 6 insertions(+), 11 deletions(-)

diff --git a/src/components/views/dialogs/NewSessionReviewDialog.js b/src/components/views/dialogs/NewSessionReviewDialog.js
index e17501da40..5172f29405 100644
--- a/src/components/views/dialogs/NewSessionReviewDialog.js
+++ b/src/components/views/dialogs/NewSessionReviewDialog.js
@@ -66,6 +66,10 @@ export default class NewSessionReviewDialog extends React.PureComponent {
         Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
             verificationRequestPromise: requestPromise,
             member: cli.getUser(userId),
+            onFinished: async () => {
+                const request = await requestPromise;
+                request.cancel();
+            },
         });
     }
 
diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.js
index 3a6e9a2d10..2a5b7ae699 100644
--- a/src/components/views/dialogs/VerificationRequestDialog.js
+++ b/src/components/views/dialogs/VerificationRequestDialog.js
@@ -25,11 +25,11 @@ export default class VerificationRequestDialog extends React.Component {
         verificationRequest: PropTypes.object,
         verificationRequestPromise: PropTypes.object,
         onFinished: PropTypes.func.isRequired,
+        member: PropTypes.string,
     };
 
     constructor(...args) {
         super(...args);
-        this.onFinished = this.onFinished.bind(this);
         this.state = {};
         if (this.props.verificationRequest) {
             this.state.verificationRequest = this.props.verificationRequest;
@@ -50,7 +50,7 @@ export default class VerificationRequestDialog extends React.Component {
         const title = request && request.isSelfVerification ?
             _t("Verify other session") : _t("Verification Request");
 
-        return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
+        return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
                 contentId="mx_Dialog_content"
                 title={title}
                 hasCancel={true}
@@ -64,13 +64,4 @@ export default class VerificationRequestDialog extends React.Component {
             />
         </BaseDialog>;
     }
-
-    async onFinished() {
-        this.props.onFinished();
-        let request = this.props.verificationRequest;
-        if (!request && this.props.verificationRequestPromise) {
-            request = await this.props.verificationRequestPromise;
-        }
-        request.cancel();
-    }
 }

From 5b48e13eb91760bd71b088d93da29e9e291301a9 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 04:46:15 +0000
Subject: [PATCH 314/389] add explicit link to edit devices from one's own
 UserInfo

---
 src/components/views/right_panel/UserInfo.tsx | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index a4b5cd0fbb..ea8c467d13 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -46,6 +46,7 @@ import EncryptionPanel from "./EncryptionPanel";
 import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
 import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
 import {Action} from "../../../dispatcher/actions";
+import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
 import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
 import BaseCard from "./BaseCard";
 import {E2EStatus} from "../../../utils/ShieldUtils";
@@ -1362,6 +1363,20 @@ const BasicUserInfo: React.FC<{
         }
     }
 
+    let editDevices;
+    if (member.userId == cli.getUserId()) {
+        editDevices = (<p>
+            <AccessibleButton className="mx_UserInfo_field" onClick={() => {
+                dis.dispatch({
+                    action: Action.ViewUserSettings,
+                    initialTabId: USER_SECURITY_TAB,
+                });
+            }}>
+                { _t("Edit devices") }
+            </AccessibleButton>
+        </p>)
+    }
+
     const securitySection = (
         <div className="mx_UserInfo_container">
             <h3>{ _t("Security") }</h3>
@@ -1371,6 +1386,7 @@ const BasicUserInfo: React.FC<{
                 loading={showDeviceListSpinner}
                 devices={devices}
                 userId={member.userId} /> }
+            { editDevices }
         </div>
     );
 

From aa0cca9306f2e56f134a82937c87e397346cd8b9 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 04:46:47 +0000
Subject: [PATCH 315/389] report IP of self-verification reqs

---
 .../views/toasts/VerificationRequestToast.tsx     | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx
index 8c8a74b2be..73440eb822 100644
--- a/src/components/views/toasts/VerificationRequestToast.tsx
+++ b/src/components/views/toasts/VerificationRequestToast.tsx
@@ -38,6 +38,7 @@ interface IProps {
 interface IState {
     counter: number;
     device?: DeviceInfo;
+    IP?: string;
 }
 
 export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
@@ -66,9 +67,15 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
         // a toast hanging around after logging in if you did a verification as part of login).
         this._checkRequestIsPending();
 
+
         if (request.isSelfVerification) {
             const cli = MatrixClientPeg.get();
-            this.setState({device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId)});
+            const device = await cli.getDevice(request.channel.deviceId);
+            const IP = device.last_seen_ip;
+            this.setState({
+                device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId),
+                IP,
+            });
         }
     }
 
@@ -118,6 +125,9 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
                 const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog");
                 Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
                     verificationRequest: request,
+                    onFinished: () => {
+                        request.cancel();
+                    },
                 }, null, /* priority = */ false, /* static = */ true);
             }
             await request.accept();
@@ -131,9 +141,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
         let nameLabel;
         if (request.isSelfVerification) {
             if (this.state.device) {
-                nameLabel = _t("From %(deviceName)s (%(deviceId)s)", {
+                nameLabel = _t("From %(deviceName)s (%(deviceId)s) from %(IP)s", {
                     deviceName: this.state.device.getDisplayName(),
                     deviceId: this.state.device.deviceId,
+                    IP: this.state.IP,
                 });
             }
         } else {

From d477f964d200993641f29b28752fc3d81a9c9e06 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 04:49:59 +0000
Subject: [PATCH 316/389] only prompt to verify if we have an MSK or we have
 devices to verify against

---
 src/stores/SetupEncryptionStore.js | 21 +++++++++++++++++++--
 1 file changed, 19 insertions(+), 2 deletions(-)

diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js
index 981ce6eca9..525678f9a3 100644
--- a/src/stores/SetupEncryptionStore.js
+++ b/src/stores/SetupEncryptionStore.js
@@ -49,6 +49,7 @@ export class SetupEncryptionStore extends EventEmitter {
         cli.on("crypto.verification.request", this.onVerificationRequest);
         cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged);
 
+
         const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId());
         if (requestsInProgress.length) {
             // If there are multiple, we take the most recent. Equally if the user sends another request from
@@ -75,7 +76,8 @@ export class SetupEncryptionStore extends EventEmitter {
     }
 
     async fetchKeyInfo() {
-        const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false);
+        const cli = MatrixClientPeg.get();
+        const keys = await cli.isSecretStored('m.cross_signing.master', false);
         if (keys === null || Object.keys(keys).length === 0) {
             this.keyId = null;
             this.keyInfo = null;
@@ -85,7 +87,22 @@ export class SetupEncryptionStore extends EventEmitter {
             this.keyInfo = keys[this.keyId];
         }
 
-        this.phase = PHASE_INTRO;
+        // do we have any other devices which are E2EE which we can verify against?
+        const dehydratedDevice = await cli.getDehydratedDevice();
+        this.hasDevicesToVerifyAgainst = cli.getStoredDevicesForUser(cli.getUserId()).some(
+            device =>
+                device.getIdentityKey() &&
+                (!dehydratedDevice || (device.deviceId != dehydratedDevice.device_id))
+        );
+
+        if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
+            // skip before we can even render anything.
+            // XXX: this causes a dialog box flash
+            this.phase = PHASE_FINISHED;
+        }
+        else {
+            this.phase = PHASE_INTRO;
+        }
         this.emit("update");
     }
 

From c73097a5b0d48e648ffaae8b74b5949b4773335b Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 04:51:16 +0000
Subject: [PATCH 317/389] fix unhelpful 'Review...' toast wording

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

diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts
index 41717e0804..bc129ebd54 100644
--- a/src/toasts/BulkUnverifiedSessionsToast.ts
+++ b/src/toasts/BulkUnverifiedSessionsToast.ts
@@ -39,7 +39,7 @@ export const showToast = (deviceIds: Set<string>) => {
 
     ToastStore.sharedInstance().addOrReplaceToast({
         key: TOAST_KEY,
-        title: _t("Review where you’re logged in"),
+        title: _t("You have unverified logins"),
         icon: "verification_warning",
         props: {
             description: _t("Verify all your sessions to ensure your account & messages are safe"),

From 96ebbad959bf3a169a2d8911fba585bb61360919 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 04:54:44 +0000
Subject: [PATCH 318/389] switch UnverifiedSessionToast to route to check
 sessions rather than verify the new login

given the chances are that the new login will be stuck doing initial sync, and won't be in position
to be verified until its finished.
---
 src/i18n/strings/en_EN.json          | 17 +++++++--------
 src/toasts/UnverifiedSessionToast.ts | 31 +++++++++++++++-------------
 2 files changed, 24 insertions(+), 24 deletions(-)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index e8a4b86c77..0b6f68a738 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -725,7 +725,7 @@
     "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
     "Yes": "Yes",
     "No": "No",
-    "Review where you’re logged in": "Review where you’re logged in",
+    "You have unverified logins": "You have unverified logins",
     "Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
     "Review": "Review",
     "Later": "Later",
@@ -749,7 +749,8 @@
     "Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data",
     "Other users may not trust it": "Other users may not trust it",
     "New login. Was this you?": "New login. Was this you?",
-    "Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s",
+    "A new login is accessing your account: %(name)s (%(deviceID)s) from %(IP)s": "A new login is accessing your account: %(name)s (%(deviceID)s) from %(IP)s",
+    "Check your devices": "Check your devices",
     "What's new?": "What's new?",
     "What's New": "What's New",
     "Update": "Update",
@@ -974,7 +975,7 @@
     "Folder": "Folder",
     "Pin": "Pin",
     "Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
-    "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
+    "From %(deviceName)s (%(deviceId)s) from %(IP)s": "From %(deviceName)s (%(deviceId)s) from %(IP)s",
     "Decline (%(counter)s)": "Decline (%(counter)s)",
     "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
     "Remove": "Remove",
@@ -1711,6 +1712,7 @@
     "Failed to deactivate user": "Failed to deactivate user",
     "Role": "Role",
     "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
+    "Edit devices": "Edit devices",
     "Security": "Security",
     "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
     "Verify by scanning": "Verify by scanning",
@@ -2611,13 +2613,8 @@
     "Decide where your account is hosted": "Decide where your account is hosted",
     "Use Security Key or Phrase": "Use Security Key or Phrase",
     "Use Security Key": "Use Security 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.",
-    "This requires the latest %(brand)s on your other devices:": "This requires the latest %(brand)s on your other devices:",
-    "%(brand)s Web": "%(brand)s Web",
-    "%(brand)s Desktop": "%(brand)s Desktop",
-    "%(brand)s iOS": "%(brand)s iOS",
-    "%(brand)s Android": "%(brand)s Android",
-    "or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client",
+    "Verify against another session": "Verify against another session",
+    "Verify this login to access your encrypted messages and prove to others that this login is really you.": "Verify this login to access your encrypted messages and prove to others that this login is really you.",
     "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
     "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
     "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.",
diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts
index 9dedd2b137..32635e689a 100644
--- a/src/toasts/UnverifiedSessionToast.ts
+++ b/src/toasts/UnverifiedSessionToast.ts
@@ -15,38 +15,36 @@ limitations under the License.
 */
 
 import { _t } from '../languageHandler';
+import dis from "../dispatcher/dispatcher";
 import { MatrixClientPeg } from '../MatrixClientPeg';
 import Modal from '../Modal';
 import DeviceListener from '../DeviceListener';
 import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog';
 import ToastStore from "../stores/ToastStore";
 import GenericToast from "../components/views/toasts/GenericToast";
+import { Action } from "../dispatcher/actions";
+import { USER_SECURITY_TAB } from "../components/views/dialogs/UserSettingsDialog";
 
 function toastKey(deviceId: string) {
     return "unverified_session_" + deviceId;
 }
 
-export const showToast = (deviceId: string) => {
+export const showToast = async (deviceId: string) => {
     const cli = MatrixClientPeg.get();
 
     const onAccept = () => {
-        Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
-            userId: cli.getUserId(),
-            device: cli.getStoredDevice(cli.getUserId(), deviceId),
-            onFinished: (r) => {
-                if (!r) {
-                    /* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
-                    DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
-                }
-            },
-        }, null, /* priority = */ false, /* static = */ true);
+        DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
+        dis.dispatch({
+            action: Action.ViewUserSettings,
+            initialTabId: USER_SECURITY_TAB,
+        });
     };
 
     const onReject = () => {
         DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
     };
 
-    const device = cli.getStoredDevice(cli.getUserId(), deviceId);
+    const device = await cli.getDevice(deviceId);
 
     ToastStore.sharedInstance().addOrReplaceToast({
         key: toastKey(deviceId),
@@ -54,8 +52,13 @@ export const showToast = (deviceId: string) => {
         icon: "verification_warning",
         props: {
             description: _t(
-                "Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}),
-            acceptLabel: _t("Verify"),
+                "A new login is accessing your account: %(name)s (%(deviceID)s) from %(IP)s", {
+                        name: device.display_name,
+                        deviceID: deviceId,
+                        IP: device.last_seen_ip,
+                    }
+            ),
+            acceptLabel: _t("Check your devices"),
             onAccept,
             rejectLabel: _t("Later"),
             onReject,

From 997d6e12811015f3c60d3813f1018fd9d5ac10e8 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 05:08:01 +0000
Subject: [PATCH 319/389] lint

---
 src/components/structures/auth/SetupEncryptionBody.js |  4 +---
 src/stores/SetupEncryptionStore.js                    |  5 ++---
 src/toasts/UnverifiedSessionToast.ts                  | 10 ++++------
 3 files changed, 7 insertions(+), 12 deletions(-)

diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js
index d2b3fbd3b9..d40192f9c1 100644
--- a/src/components/structures/auth/SetupEncryptionBody.js
+++ b/src/components/structures/auth/SetupEncryptionBody.js
@@ -157,13 +157,11 @@ export default class SetupEncryptionBody extends React.Component {
                 </AccessibleButton>;
             }
 
-            const brand = SdkConfig.get().brand;
-
             return (
                 <div>
                     <p>{_t(
                         "Verify this login to access your encrypted messages and " +
-                        "prove to others that this login is really you."
+                        "prove to others that this login is really you.",
                     )}</p>
 
                     <div className="mx_CompleteSecurity_actionRow">
diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js
index 525678f9a3..fdabfa8019 100644
--- a/src/stores/SetupEncryptionStore.js
+++ b/src/stores/SetupEncryptionStore.js
@@ -92,15 +92,14 @@ export class SetupEncryptionStore extends EventEmitter {
         this.hasDevicesToVerifyAgainst = cli.getStoredDevicesForUser(cli.getUserId()).some(
             device =>
                 device.getIdentityKey() &&
-                (!dehydratedDevice || (device.deviceId != dehydratedDevice.device_id))
+                (!dehydratedDevice || (device.deviceId != dehydratedDevice.device_id)),
         );
 
         if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
             // skip before we can even render anything.
             // XXX: this causes a dialog box flash
             this.phase = PHASE_FINISHED;
-        }
-        else {
+        } else {
             this.phase = PHASE_INTRO;
         }
         this.emit("update");
diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts
index 32635e689a..d375ef6112 100644
--- a/src/toasts/UnverifiedSessionToast.ts
+++ b/src/toasts/UnverifiedSessionToast.ts
@@ -17,9 +17,7 @@ limitations under the License.
 import { _t } from '../languageHandler';
 import dis from "../dispatcher/dispatcher";
 import { MatrixClientPeg } from '../MatrixClientPeg';
-import Modal from '../Modal';
 import DeviceListener from '../DeviceListener';
-import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog';
 import ToastStore from "../stores/ToastStore";
 import GenericToast from "../components/views/toasts/GenericToast";
 import { Action } from "../dispatcher/actions";
@@ -53,10 +51,10 @@ export const showToast = async (deviceId: string) => {
         props: {
             description: _t(
                 "A new login is accessing your account: %(name)s (%(deviceID)s) from %(IP)s", {
-                        name: device.display_name,
-                        deviceID: deviceId,
-                        IP: device.last_seen_ip,
-                    }
+                    name: device.display_name,
+                    deviceID: deviceId,
+                    IP: device.last_seen_ip,
+                },
             ),
             acceptLabel: _t("Check your devices"),
             onAccept,

From 7abc48a71679de4afa37fe73d8edf966f29af43e Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 05:30:02 +0000
Subject: [PATCH 320/389] lint

---
 src/components/structures/auth/SetupEncryptionBody.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js
index d40192f9c1..4e3d106eb1 100644
--- a/src/components/structures/auth/SetupEncryptionBody.js
+++ b/src/components/structures/auth/SetupEncryptionBody.js
@@ -17,7 +17,6 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
-import SdkConfig from '../../../SdkConfig';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
 import Modal from '../../../Modal';
 import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';

From f5458665721d1c241ff004a96c8b572e3ad87ae1 Mon Sep 17 00:00:00 2001
From: Helder Ferreira <me@helderferreira.io>
Date: Mon, 8 Mar 2021 08:08:02 +0000
Subject: [PATCH 321/389] add clipboard write permission

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

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 213351889f..d290dbeaff 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -325,7 +325,7 @@ export default class AppTile extends React.Component {
 
         // Additional iframe feature pemissions
         // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
-        const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture;";
+        const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write;";
 
         const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini  ' : ' ');
 

From df4631d65ba5e7b213e798f226102cf4de5ce05b Mon Sep 17 00:00:00 2001
From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
Date: Mon, 8 Mar 2021 15:00:09 +0000
Subject: [PATCH 322/389] Fix some ineffective placeholders in a couple log
 lines (#5729)

Was producing `Presence: %s unavailable` in the logs.
---
 src/Presence.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Presence.ts b/src/Presence.ts
index 660bb0ac94..eb56c5714e 100644
--- a/src/Presence.ts
+++ b/src/Presence.ts
@@ -99,9 +99,9 @@ class Presence {
 
         try {
             await MatrixClientPeg.get().setPresence(this.state);
-            console.info("Presence: %s", newState);
+            console.info("Presence:", newState);
         } catch (err) {
-            console.error("Failed to set presence: %s", err);
+            console.error("Failed to set presence:", err);
             this.state = oldState;
         }
     }

From fdea45ad74905346744e5a4c5c77bd3ddf64d3f1 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 08:51:57 -0700
Subject: [PATCH 323/389] Add a quick sender check to isLastSuccessful

---
 src/components/structures/MessagePanel.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index c1ccf61470..9deda54bee 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -604,6 +604,10 @@ export default class MessagePanel extends React.Component {
             isLastSuccessful = true;
         }
 
+        // We only want to consider "last successful" if the event is sent by us, otherwise of course
+        // it's successful: we received it.
+        isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
+
         // use txnId as key if available so that we don't remount during sending
         ret.push(
             <li

From 6a5efad1424c7b4accbfb4f83ec0f37b728c5f0c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 8 Mar 2021 15:52:21 +0000
Subject: [PATCH 324/389] Show suggested rooms from the selected space

---
 src/components/views/rooms/RoomList.tsx      | 74 +++++++++++++++++++-
 src/components/views/rooms/TemporaryTile.tsx | 19 ++---
 src/i18n/strings/en_EN.json                  |  2 +
 src/stores/SpaceStore.tsx                    | 33 ++++++++-
 src/stores/room-list/models.ts               |  2 +
 5 files changed, 116 insertions(+), 14 deletions(-)

diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index f7da6571da..beb85e50ce 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -19,6 +19,7 @@ limitations under the License.
 import * as React from "react";
 import { Dispatcher } from "flux";
 import { Room } from "matrix-js-sdk/src/models/room";
+import * as fbEmitter from "fbemitter";
 
 import { _t, _td } from "../../../languageHandler";
 import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
@@ -47,9 +48,11 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
 import AccessibleButton from "../elements/AccessibleButton";
 import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
 import CallHandler from "../../../CallHandler";
-import SpaceStore from "../../../stores/SpaceStore";
+import SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore";
 import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
 import { EventType } from "matrix-js-sdk/src/@types/event";
+import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
+import RoomAvatar from "../avatars/RoomAvatar";
 
 interface IProps {
     onKeyDown: (ev: React.KeyboardEvent) => void;
@@ -63,6 +66,8 @@ interface IProps {
 interface IState {
     sublists: ITagMap;
     isNameFiltering: boolean;
+    currentRoomId?: string;
+    suggestedRooms: ISpaceSummaryRoom[];
 }
 
 const TAG_ORDER: TagID[] = [
@@ -75,6 +80,7 @@ const TAG_ORDER: TagID[] = [
 
     DefaultTagID.LowPriority,
     DefaultTagID.ServerNotice,
+    DefaultTagID.Suggested,
     DefaultTagID.Archived,
 ];
 const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
@@ -242,6 +248,12 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
         isInvite: false,
         defaultHidden: true,
     },
+
+    [DefaultTagID.Suggested]: {
+        sectionLabel: _td("Suggested Rooms"),
+        isInvite: false,
+        defaultHidden: false,
+    },
 };
 
 function customTagAesthetics(tagId: TagID): ITagAesthetics {
@@ -260,6 +272,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
     private dispatcherRef;
     private customTagStoreRef;
     private tagAesthetics: ITagAestheticsMap;
+    private roomStoreToken: fbEmitter.EventSubscription;
 
     constructor(props: IProps) {
         super(props);
@@ -267,6 +280,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
         this.state = {
             sublists: {},
             isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
+            suggestedRooms: SpaceStore.instance.suggestedRooms,
         };
 
         // shallow-copy from the template as we need to make modifications to it
@@ -274,20 +288,30 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
         this.updateDmAddRoomAction();
 
         this.dispatcherRef = defaultDispatcher.register(this.onAction);
+        this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
     }
 
     public componentDidMount(): void {
+        SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms);
         RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
         this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
         this.updateLists(); // trigger the first update
     }
 
     public componentWillUnmount() {
+        SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms);
         RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
         defaultDispatcher.unregister(this.dispatcherRef);
         if (this.customTagStoreRef) this.customTagStoreRef.remove();
+        if (this.roomStoreToken) this.roomStoreToken.remove();
     }
 
+    private onRoomViewStoreUpdate = () => {
+        this.setState({
+            currentRoomId: RoomViewStore.getRoomId(),
+        });
+    };
+
     private updateDmAddRoomAction() {
         const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]);
         if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
@@ -319,7 +343,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
 
     private getRoomDelta = (roomId: string, delta: number, unread = false) => {
         const lists = RoomListStore.instance.orderedLists;
-        const rooms: Room = [];
+        const rooms: Room[] = [];
         TAG_ORDER.forEach(t => {
             let listRooms = lists[t];
 
@@ -340,6 +364,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
         return room;
     };
 
+    private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => {
+        this.setState({ suggestedRooms });
+    };
+
     private updateLists = () => {
         const newLists = RoomListStore.instance.orderedLists;
         if (SettingsStore.getValue("advancedRoomListLogging")) {
@@ -394,6 +422,39 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
         dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
     };
 
+    private renderSuggestedRooms(): JSX.Element[] {
+        return this.state.suggestedRooms.map(room => {
+            const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
+            const avatar = (
+                <RoomAvatar
+                    oobData={{
+                        name,
+                        avatarUrl: room.avatar_url,
+                    }}
+                    width={32}
+                    height={32}
+                    resizeMethod="crop"
+                />
+            );
+            const viewRoom = () => {
+                defaultDispatcher.dispatch({
+                    action: "view_room",
+                    room_id: room.room_id,
+                });
+            };
+            return (
+                <TemporaryTile
+                    isMinimized={this.props.isMinimized}
+                    isSelected={this.state.currentRoomId === room.room_id}
+                    displayName={name}
+                    avatar={avatar}
+                    onClick={viewRoom}
+                    key={`suggestedRoomTile_${room.room_id}`}
+                />
+            );
+        });
+    }
+
     private renderCommunityInvites(): TemporaryTile[] {
         // TODO: Put community invites in a more sensible place (not in the room list)
         // See https://github.com/vector-im/element-web/issues/14456
@@ -447,7 +508,14 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
 
         for (const orderedTagId of tagOrder) {
             const orderedRooms = this.state.sublists[orderedTagId] || [];
-            const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
+
+            let extraTiles = null;
+            if (orderedTagId === DefaultTagID.Invite) {
+                extraTiles = this.renderCommunityInvites();
+            } else if (orderedTagId === DefaultTagID.Suggested) {
+                extraTiles = this.renderSuggestedRooms();
+            }
+
             const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
             if (totalTiles === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
                 continue; // skip tag - not needed
diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx
index eec3105880..31d2acbc61 100644
--- a/src/components/views/rooms/TemporaryTile.tsx
+++ b/src/components/views/rooms/TemporaryTile.tsx
@@ -28,7 +28,7 @@ interface IProps {
     isSelected: boolean;
     displayName: string;
     avatar: React.ReactElement;
-    notificationState: NotificationState;
+    notificationState?: NotificationState;
     onClick: () => void;
 }
 
@@ -63,12 +63,15 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
             'mx_RoomTile_minimized': this.props.isMinimized,
         });
 
-        const badge = (
-            <NotificationBadge
-                notification={this.props.notificationState}
-                forceCount={false}
-            />
-        );
+        let badge;
+        if (this.props.notificationState) {
+            badge = (
+                <NotificationBadge
+                    notification={this.props.notificationState}
+                    forceCount={false}
+                />
+            );
+        }
 
         let name = this.props.displayName;
         if (typeof name !== 'string') name = '';
@@ -76,7 +79,7 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
 
         const nameClasses = classNames({
             "mx_RoomTile_name": true,
-            "mx_RoomTile_nameHasUnreadEvents": this.props.notificationState.isUnread,
+            "mx_RoomTile_nameHasUnreadEvents": this.props.notificationState?.isUnread,
         });
 
         let nameContainer = (
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 7b680b6590..71aae7fecd 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1529,7 +1529,9 @@
     "Low priority": "Low priority",
     "System Alerts": "System Alerts",
     "Historical": "Historical",
+    "Suggested Rooms": "Suggested Rooms",
     "Custom Tag": "Custom Tag",
+    "Empty room": "Empty room",
     "Can't see what you’re looking for?": "Can't see what you’re looking for?",
     "Start a new chat": "Start a new chat",
     "Explore all public rooms": "Explore all public rooms",
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index c334144d70..1ada5d6361 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import {throttle, sortBy} from "lodash";
-import {EventType} from "matrix-js-sdk/src/@types/event";
+import {sortBy, throttle} from "lodash";
+import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
 import {Room} from "matrix-js-sdk/src/models/room";
 import {MatrixEvent} from "matrix-js-sdk/src/models/event";
 
@@ -33,6 +33,7 @@ import {EnhancedMap, mapDiff} from "../utils/maps";
 import {setHasDiff} from "../utils/sets";
 import {objectDiff} from "../utils/objects";
 import {arrayHasDiff} from "../utils/arrays";
+import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
 
 type SpaceKey = string | symbol;
 
@@ -41,11 +42,14 @@ interface IState {}
 const ACTIVE_SPACE_LS_KEY = "mx_active_space";
 
 export const HOME_SPACE = Symbol("home-space");
+export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
 
 export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
 export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
 // Space Room ID/HOME_SPACE will be emitted when a Space's children change
 
+const MAX_SUGGESTED_ROOMS = 20;
+
 const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
     return arr.reduce((result, room: Room) => {
         result[room.isSpaceRoom() ? 0 : 1].push(room);
@@ -85,6 +89,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
     private spaceFilteredRooms = new Map<string | symbol, Set<string>>();
     // The space currently selected in the Space Panel - if null then `Home` is selected
     private _activeSpace?: Room = null;
+    private _suggestedRooms: ISpaceSummaryRoom[] = [];
 
     public get spacePanelSpaces(): Room[] {
         return this.rootSpaces;
@@ -94,11 +99,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
         return this._activeSpace || null;
     }
 
-    public setActiveSpace(space: Room | null) {
+    public get suggestedRooms(): ISpaceSummaryRoom[] {
+        return this._suggestedRooms;
+    }
+
+    public async setActiveSpace(space: Room | null) {
         if (space === this.activeSpace) return;
 
         this._activeSpace = space;
         this.emit(UPDATE_SELECTED_SPACE, this.activeSpace);
+        this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []);
 
         // persist space selected
         if (space) {
@@ -106,6 +116,23 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
         } else {
             window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY);
         }
+
+        if (space) {
+            try {
+                const data: {
+                    rooms: ISpaceSummaryRoom[];
+                    events: ISpaceSummaryEvent[];
+                } = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, MAX_SUGGESTED_ROOMS);
+                if (this._activeSpace === space) {
+                    this._suggestedRooms = data.rooms.filter(roomInfo => {
+                        return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id);
+                    });
+                    this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
+                }
+            } catch (e) {
+                console.error(e);
+            }
+        }
     }
 
     public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) {
diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts
index 7d3902f552..54d49ea18a 100644
--- a/src/stores/room-list/models.ts
+++ b/src/stores/room-list/models.ts
@@ -24,6 +24,7 @@ export enum DefaultTagID {
     Favourite = "m.favourite",
     DM = "im.vector.fake.direct",
     ServerNotice = "m.server_notice",
+    Suggested = "im.vector.fake.suggested",
 }
 
 export const OrderedDefaultTagIDs = [
@@ -33,6 +34,7 @@ export const OrderedDefaultTagIDs = [
     DefaultTagID.Untagged,
     DefaultTagID.LowPriority,
     DefaultTagID.ServerNotice,
+    DefaultTagID.Suggested,
     DefaultTagID.Archived,
 ];
 

From 1629b7e62a3179ca756717bd60cf25746871b16d Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 16:44:14 +0000
Subject: [PATCH 325/389] s/IP/ip/; s/from/at/

---
 .../views/toasts/VerificationRequestToast.tsx          | 10 +++++-----
 src/i18n/strings/en_EN.json                            |  4 ++--
 src/toasts/UnverifiedSessionToast.ts                   |  4 ++--
 3 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx
index 73440eb822..82c6b0b952 100644
--- a/src/components/views/toasts/VerificationRequestToast.tsx
+++ b/src/components/views/toasts/VerificationRequestToast.tsx
@@ -38,7 +38,7 @@ interface IProps {
 interface IState {
     counter: number;
     device?: DeviceInfo;
-    IP?: string;
+    ip?: string;
 }
 
 export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
@@ -71,10 +71,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
         if (request.isSelfVerification) {
             const cli = MatrixClientPeg.get();
             const device = await cli.getDevice(request.channel.deviceId);
-            const IP = device.last_seen_ip;
+            const ip = device.last_seen_ip;
             this.setState({
                 device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId),
-                IP,
+                ip,
             });
         }
     }
@@ -141,10 +141,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
         let nameLabel;
         if (request.isSelfVerification) {
             if (this.state.device) {
-                nameLabel = _t("From %(deviceName)s (%(deviceId)s) from %(IP)s", {
+                nameLabel = _t("From %(deviceName)s (%(deviceId)s) at %(ip)s", {
                     deviceName: this.state.device.getDisplayName(),
                     deviceId: this.state.device.deviceId,
-                    IP: this.state.IP,
+                    ip: this.state.ip,
                 });
             }
         } else {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 51d3561e2c..98a0ceb5d1 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -753,7 +753,7 @@
     "Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data",
     "Other users may not trust it": "Other users may not trust it",
     "New login. Was this you?": "New login. Was this you?",
-    "A new login is accessing your account: %(name)s (%(deviceID)s) from %(IP)s": "A new login is accessing your account: %(name)s (%(deviceID)s) from %(IP)s",
+    "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s",
     "Check your devices": "Check your devices",
     "What's new?": "What's new?",
     "What's New": "What's New",
@@ -981,7 +981,7 @@
     "Folder": "Folder",
     "Pin": "Pin",
     "Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
-    "From %(deviceName)s (%(deviceId)s) from %(IP)s": "From %(deviceName)s (%(deviceId)s) from %(IP)s",
+    "From %(deviceName)s (%(deviceId)s) at %(ip)s": "From %(deviceName)s (%(deviceId)s) at %(ip)s",
     "Decline (%(counter)s)": "Decline (%(counter)s)",
     "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
     "Delete": "Delete",
diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts
index d375ef6112..e0ea323033 100644
--- a/src/toasts/UnverifiedSessionToast.ts
+++ b/src/toasts/UnverifiedSessionToast.ts
@@ -50,10 +50,10 @@ export const showToast = async (deviceId: string) => {
         icon: "verification_warning",
         props: {
             description: _t(
-                "A new login is accessing your account: %(name)s (%(deviceID)s) from %(IP)s", {
+                "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s", {
                     name: device.display_name,
                     deviceID: deviceId,
-                    IP: device.last_seen_ip,
+                    ip: device.last_seen_ip,
                 },
             ),
             acceptLabel: _t("Check your devices"),

From 14b828ecc63652d7aac081c28bafd07fa8119d61 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 17:58:30 +0000
Subject: [PATCH 326/389] shorten verify button label

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

diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js
index 4e3d106eb1..431762cade 100644
--- a/src/components/structures/auth/SetupEncryptionBody.js
+++ b/src/components/structures/auth/SetupEncryptionBody.js
@@ -152,7 +152,7 @@ export default class SetupEncryptionBody extends React.Component {
             let verifyButton;
             if (store.hasDevicesToVerifyAgainst) {
                 verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
-                    { _t("Verify against another session") }
+                    { _t("Verify with another session") }
                 </AccessibleButton>;
             }
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 98a0ceb5d1..68664fa0dd 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2714,7 +2714,7 @@
     "Decide where your account is hosted": "Decide where your account is hosted",
     "Use Security Key or Phrase": "Use Security Key or Phrase",
     "Use Security Key": "Use Security Key",
-    "Verify against another session": "Verify against another session",
+    "Verify with another session": "Verify with another session",
     "Verify this login to access your encrypted messages and prove to others that this login is really you.": "Verify this login to access your encrypted messages and prove to others that this login is really you.",
     "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
     "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",

From b3cd6fcc9bf11d5070dd5697ee8756dc6bde34f2 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 17:59:02 +0000
Subject: [PATCH 327/389] vertically align labels in AccessibleButtons if their
 height is pushed out by wrapped text elsewhere

---
 res/css/views/elements/_AccessibleButton.scss | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss
index 9c26f8f120..b115130c8f 100644
--- a/res/css/views/elements/_AccessibleButton.scss
+++ b/res/css/views/elements/_AccessibleButton.scss
@@ -26,7 +26,8 @@ limitations under the License.
     padding: 7px 18px;
     text-align: center;
     border-radius: 8px;
-    display: inline-block;
+    display: flex;
+    align-items: center;
     font-size: $font-14px;
 }
 

From 9d99b2f239e1bb6c7179239aa0729bbac122f987 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 17:59:10 +0000
Subject: [PATCH 328/389] remove errand whitespace

---
 src/stores/SetupEncryptionStore.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js
index fdabfa8019..2ed778b294 100644
--- a/src/stores/SetupEncryptionStore.js
+++ b/src/stores/SetupEncryptionStore.js
@@ -49,7 +49,6 @@ export class SetupEncryptionStore extends EventEmitter {
         cli.on("crypto.verification.request", this.onVerificationRequest);
         cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged);
 
-
         const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId());
         if (requestsInProgress.length) {
             // If there are multiple, we take the most recent. Equally if the user sends another request from

From 92c8b697d21607cb9a5e3093d0db550f1bc2dfd5 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Mon, 8 Mar 2021 18:55:33 +0000
Subject: [PATCH 329/389] Fix units of TURN server expiry time

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

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 8621f441de..d2564e637b 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -630,7 +630,7 @@ export default class CallHandler {
         logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
 
         const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
-        console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " seconds");
+        console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
         const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
 
         this.calls.set(roomId, call);

From 7963d7f49ebc7da5387bd8037f14ce13ae4f55cd Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Mon, 8 Mar 2021 21:21:37 +0200
Subject: [PATCH 330/389] address PR comments

cleanup, change to isEncrypted, comments
---
 .../views/context_menus/MessageContextMenu.js     | 15 ++-------------
 .../views/messages/EditHistoryMessage.js          |  3 ++-
 2 files changed, 4 insertions(+), 14 deletions(-)

diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index b002d1ec62..98d0aad578 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -130,20 +130,9 @@ export default class MessageContextMenu extends React.Component {
             roomId: ev.getRoomId(),
             eventId: ev.getId(),
             content: ev.event,
-            isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(),
-            decryptedContent: ev._clearEvent,
-        }, 'mx_Dialog_viewsource');
-        this.closeMenu();
-    };
-
-    onViewClearSourceClick = () => {
-        const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
-        const ViewSource = sdk.getComponent('structures.ViewSource');
-        Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
-            roomId: ev.getRoomId(),
-            eventId: ev.getId(),
+            isEncrypted: ev.isEncrypted(),
             // FIXME: _clearEvent is private
-            content: ev._clearEvent,
+            decryptedContent: ev._clearEvent,
         }, 'mx_Dialog_viewsource');
         this.closeMenu();
     };
diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
index 68a3c95745..f37efe4800 100644
--- a/src/components/views/messages/EditHistoryMessage.js
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -77,7 +77,8 @@ export default class EditHistoryMessage extends React.PureComponent {
             roomId: this.props.mxEvent.getRoomId(),
             eventId: this.props.mxEvent.getId(),
             content: this.props.mxEvent.event,
-            isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(),
+            isEncrypted: this.props.mxEvent.isEncrypted(),
+            // FIXME: _clearEvent is private
             decryptedContent: this.props.mxEvent._clearEvent,
         }, 'mx_Dialog_viewsource');
     };

From 9287e8dfa4f55b368a3c108d3c15442cdfdc4c1c Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Mon, 8 Mar 2021 22:15:34 +0200
Subject: [PATCH 331/389] use isEncrypted, edit state events

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

diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index ddcffe4f7f..cfe28e9f73 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -51,7 +51,7 @@ export default class ViewSource extends React.Component {
     // returns the dialog body for viewing the event source
     viewSourceContent() {
         const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
-        const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType();
+        const isEncrypted = mxEvent.isEncrypted();
         const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private
         const originalEventSource = mxEvent.event;
 
@@ -85,7 +85,7 @@ export default class ViewSource extends React.Component {
     // returns the id of the initial message, not the id of the previous edit
     getBaseEventId() {
         const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
-        const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType();
+        const isEncrypted = mxEvent.isEncrypted();
         const baseMxEvent = this.props.mxEvent;
 
         if (isEncrypted) {
@@ -163,7 +163,7 @@ export default class ViewSource extends React.Component {
         const isEditing = this.state.isEditing;
         const roomId = mxEvent.getRoomId();
         const eventId = mxEvent.getId();
-        const canEdit = canEditContent(this.props.mxEvent);
+        const canEdit = canEditContent(this.props.mxEvent) || mxEvent.isState();
         return (
             <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
                 <div>

From c543f0a2d0d7178e6c7efb271315c674b443fb28 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 22:29:08 +0000
Subject: [PATCH 332/389] fix AccessibleButton label positioning now they can
 wrap

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

diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss
index b115130c8f..e13f765e63 100644
--- a/res/css/views/elements/_AccessibleButton.scss
+++ b/res/css/views/elements/_AccessibleButton.scss
@@ -28,6 +28,7 @@ limitations under the License.
     border-radius: 8px;
     display: flex;
     align-items: center;
+    justify-content: center;
     font-size: $font-14px;
 }
 

From bb8fe62c5c77ebaa407e42b2fdb8f5228911a29a Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Mon, 8 Mar 2021 23:11:44 +0000
Subject: [PATCH 333/389] Translated using Weblate (German)

Currently translated at 99.5% (2767 of 2780 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 | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 07a1a4f805..7304d35fcd 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -1433,7 +1433,7 @@
     "Never send encrypted messages to unverified sessions in this room from this session": "Sende niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen in diesem Raum",
     "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Durch die Änderung des Passworts werden derzeit alle Ende-zu-Ende-Verschlüsselungsschlüssel in allen Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist, es sei denn, du exportierst zuerst deine Raumschlüssel und importierst sie anschließend wieder. In Zukunft wird dies verbessert werden.",
     "Delete %(count)s sessions|other": "%(count)s Sitzungen löschen",
-    "Backup is not signed by any of your sessions": "Die Sicherung wurde von keiner deiner Sitzungen unterzeichnet",
+    "Backup is not signed by any of your sessions": "Die Sicherung wurde von keiner deiner Sitzungen bestätigt",
     "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du erhältst keine Push-Benachrichtigungen zu anderen Sitzungen, bis du dich wieder bei diesen anmeldest",
     "Notification sound": "Benachrichtigungston",
     "Set a new custom sound": "Setze einen neuen benutzerdefinierten Ton",
@@ -2060,7 +2060,7 @@
     "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 kann nicht geteilt werden",
-    "Please enter verification code sent via text.": "Gib den Verifikationscode ein, den du empfangen hast.",
+    "Please enter verification code sent via text.": "Gib den Bestätigungscode ein, den du empfangen hast.",
     "Almost there! Is your other session showing the same shield?": "Fast geschafft! Zeigt deine andere Sitzung das gleiche Schild?",
     "Almost there! Is %(displayName)s showing the same shield?": "Fast geschafft! Wird bei %(displayName)s das gleiche Schild angezeigt?",
     "Click the link in the email you received to verify and then click continue again.": "Klicke auf den Link in der Bestätigungs-E-Mail, und dann auf Weiter.",
@@ -3072,5 +3072,9 @@
     "Failed to save settings": "Einstellungen konnten nicht gespeichert werden",
     "Show chat effects (animations when receiving e.g. confetti)": "Animierte Chateffekte zeigen, wenn z.B. Konfetti-Emojis erhalten werden",
     "Save setting values": "Einstellungswerte speichern",
-    "Caution:": "Vorsicht:"
+    "Caution:": "Vorsicht:",
+    "Settable at global": "Global festlegbar",
+    "Setting definition:": "Definition der Einstellung:",
+    "Value in this room": "Wert in diesem Raum",
+    "Settings Explorer": "Einstellungs-Explorer"
 }

From d388f877b463cdc16dfb5e4cb0e649502871af81 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 23:28:44 +0000
Subject: [PATCH 334/389] add PHASE_LOADING to SetupEncryptionStore to avoid
 flashing cross-signing setup

---
 src/components/structures/auth/CompleteSecurity.js  |  5 ++++-
 .../structures/auth/SetupEncryptionBody.js          |  3 ++-
 src/stores/SetupEncryptionStore.js                  | 13 +++++++------
 3 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js
index c73691611d..b18776e0ea 100644
--- a/src/components/structures/auth/CompleteSecurity.js
+++ b/src/components/structures/auth/CompleteSecurity.js
@@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
 import * as sdk from '../../../index';
 import {
     SetupEncryptionStore,
+    PHASE_LOADING,
     PHASE_INTRO,
     PHASE_BUSY,
     PHASE_DONE,
@@ -58,7 +59,9 @@ export default class CompleteSecurity extends React.Component {
         let icon;
         let title;
 
-        if (phase === PHASE_INTRO) {
+        if (phase === PHASE_LOADING) {
+            return null;
+        } else if (phase === PHASE_INTRO) {
             icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
             title = _t("Verify this login");
         } else if (phase === PHASE_DONE) {
diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js
index 431762cade..32f0f41024 100644
--- a/src/components/structures/auth/SetupEncryptionBody.js
+++ b/src/components/structures/auth/SetupEncryptionBody.js
@@ -23,6 +23,7 @@ import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDi
 import * as sdk from '../../../index';
 import {
     SetupEncryptionStore,
+    PHASE_LOADING,
     PHASE_INTRO,
     PHASE_BUSY,
     PHASE_DONE,
@@ -222,7 +223,7 @@ export default class SetupEncryptionBody extends React.Component {
                     </div>
                 </div>
             );
-        } else if (phase === PHASE_BUSY) {
+        } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
             const Spinner = sdk.getComponent('views.elements.Spinner');
             return <Spinner />;
         } else {
diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js
index 2ed778b294..28ab76edc0 100644
--- a/src/stores/SetupEncryptionStore.js
+++ b/src/stores/SetupEncryptionStore.js
@@ -19,11 +19,12 @@ import { MatrixClientPeg } from '../MatrixClientPeg';
 import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
 import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 
-export const PHASE_INTRO = 0;
-export const PHASE_BUSY = 1;
-export const PHASE_DONE = 2;    //final done stage, but still showing UX
-export const PHASE_CONFIRM_SKIP = 3;
-export const PHASE_FINISHED = 4; //UX can be closed
+export const PHASE_LOADING = 0;
+export const PHASE_INTRO = 1;
+export const PHASE_BUSY = 2;
+export const PHASE_DONE = 3;    //final done stage, but still showing UX
+export const PHASE_CONFIRM_SKIP = 4;
+export const PHASE_FINISHED = 5; //UX can be closed
 
 export class SetupEncryptionStore extends EventEmitter {
     static sharedInstance() {
@@ -36,7 +37,7 @@ export class SetupEncryptionStore extends EventEmitter {
             return;
         }
         this._started = true;
-        this.phase = PHASE_BUSY;
+        this.phase = PHASE_LOADING;
         this.verificationRequest = null;
         this.backupInfo = null;
 

From 72f28240aa7fc274e70035af24ce908633fc1630 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Mon, 8 Mar 2021 23:28:54 +0000
Subject: [PATCH 335/389] fix AccessibleButton layout some more

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

diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss
index e13f765e63..0075dcb511 100644
--- a/res/css/views/elements/_AccessibleButton.scss
+++ b/res/css/views/elements/_AccessibleButton.scss
@@ -26,7 +26,7 @@ limitations under the License.
     padding: 7px 18px;
     text-align: center;
     border-radius: 8px;
-    display: flex;
+    display: inline-flex;
     align-items: center;
     justify-content: center;
     font-size: $font-14px;

From 92af111c930a067af0a287338f173cb1b876fb3e Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 19:19:52 -0700
Subject: [PATCH 336/389] Fix types for replaceableComponent

This is to make it work in TS files
---
 src/utils/replaceableComponent.ts | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/utils/replaceableComponent.ts b/src/utils/replaceableComponent.ts
index 8c29fdf037..f8dd5f8ac6 100644
--- a/src/utils/replaceableComponent.ts
+++ b/src/utils/replaceableComponent.ts
@@ -30,9 +30,11 @@ import * as sdk from '../index';
  * @param {string} name The dot-path name of the component being replaced.
  * @param {React.Component} origComponent The component that can be replaced
  * with a skinned version. If no skinned version is available, this component
- * will be used.
+ * will be used. Note that this is automatically provided to the function and
+ * thus is optional for purposes of types.
+ * @returns {ClassDecorator} The decorator.
  */
-export function replaceableComponent(name: string, origComponent: React.Component) {
+export function replaceableComponent(name: string, origComponent?: React.Component): ClassDecorator {
     // Decorators return a function to override the class (origComponent). This
     // ultimately assumes that `getComponent()` won't throw an error and instead
     // return a falsey value like `null` when the skin doesn't have a component.

From a5f237dfd6094decab5defe71c75ba608973d262 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 19:33:10 -0700
Subject: [PATCH 337/389] Make debugging skinning problems easier

---
 src/Skinner.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Skinner.js b/src/Skinner.js
index d17bc1782a..ef340e4052 100644
--- a/src/Skinner.js
+++ b/src/Skinner.js
@@ -23,7 +23,7 @@ class Skinner {
         if (!name) throw new Error(`Invalid component name: ${name}`);
         if (this.components === null) {
             throw new Error(
-                "Attempted to get a component before a skin has been loaded."+
+                `Attempted to get a component (${name}) before a skin has been loaded.`+
                 " This is probably because either:"+
                 " a) Your app has not called sdk.loadSkin(), or"+
                 " b) A component has called getComponent at the root level",

From c230a75eda74622df6b923eb6091286740094637 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 19:35:10 -0700
Subject: [PATCH 338/389] Flag structural components as replaceable

---
 src/components/structures/ContextMenu.tsx             | 3 +++
 src/components/structures/CustomRoomTagPanel.js       | 2 ++
 src/components/structures/FilePanel.js                | 2 ++
 src/components/structures/GenericErrorPage.js         | 2 ++
 src/components/structures/GroupFilterPanel.js         | 2 ++
 src/components/structures/GroupView.js                | 2 ++
 src/components/structures/HostSignupAction.tsx        | 2 ++
 src/components/structures/IndicatorScrollbar.js       | 2 ++
 src/components/structures/InteractiveAuth.js          | 2 ++
 src/components/structures/LeftPanel.tsx               | 2 ++
 src/components/structures/LoggedInView.tsx            | 2 ++
 src/components/structures/MainSplit.js                | 2 ++
 src/components/structures/MatrixChat.tsx              | 2 ++
 src/components/structures/MessagePanel.js             | 2 ++
 src/components/structures/MyGroups.js                 | 2 ++
 src/components/structures/NonUrgentToastContainer.tsx | 2 ++
 src/components/structures/NotificationPanel.js        | 2 ++
 src/components/structures/RightPanel.js               | 2 ++
 src/components/structures/RoomDirectory.js            | 2 ++
 src/components/structures/RoomSearch.tsx              | 2 ++
 src/components/structures/RoomStatusBar.js            | 2 ++
 src/components/structures/RoomView.tsx                | 2 ++
 src/components/structures/ScrollPanel.js              | 2 ++
 src/components/structures/SearchBox.js                | 2 ++
 src/components/structures/TabbedView.tsx              | 2 ++
 src/components/structures/TimelinePanel.js            | 2 ++
 src/components/structures/ToastContainer.tsx          | 2 ++
 src/components/structures/UploadBar.tsx               | 2 ++
 src/components/structures/UserMenu.tsx                | 2 ++
 src/components/structures/UserView.js                 | 2 ++
 src/components/structures/ViewSource.js               | 2 ++
 src/components/structures/auth/CompleteSecurity.js    | 2 ++
 src/components/structures/auth/E2eSetup.js            | 2 ++
 src/components/structures/auth/ForgotPassword.js      | 2 ++
 src/components/structures/auth/Login.tsx              | 2 ++
 src/components/structures/auth/Registration.tsx       | 2 ++
 src/components/structures/auth/SetupEncryptionBody.js | 2 ++
 src/components/structures/auth/SoftLogout.js          | 2 ++
 38 files changed, 77 insertions(+)

diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index 726ff547ff..9d9d57d8a6 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -22,6 +22,7 @@ import classNames from "classnames";
 
 import {Key} from "../../Keyboard";
 import {Writeable} from "../../@types/common";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 // Shamelessly ripped off Modal.js.  There's probably a better way
 // of doing reusable widgets like dialog boxes & menus where we go and
@@ -91,6 +92,7 @@ interface IState {
 // Generic ContextMenu Portal wrapper
 // all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
 // this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
+@replaceableComponent("structures.ContextMenu")
 export class ContextMenu extends React.PureComponent<IProps, IState> {
     private initialFocus: HTMLElement;
 
@@ -467,6 +469,7 @@ export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<
     return [isOpen, button, open, close, setIsOpen];
 };
 
+@replaceableComponent("structures.LegacyContextMenu")
 export default class LegacyContextMenu extends ContextMenu {
     render() {
         return this.renderMenu(false);
diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js
index a79bdafeb5..73359f17a5 100644
--- a/src/components/structures/CustomRoomTagPanel.js
+++ b/src/components/structures/CustomRoomTagPanel.js
@@ -21,7 +21,9 @@ import * as sdk from '../../index';
 import dis from '../../dispatcher/dispatcher';
 import classNames from 'classnames';
 import * as FormattingUtils from '../../utils/FormattingUtils';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.CustomRoomTagPanel")
 class CustomRoomTagPanel extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js
index 0e4df4621d..9f5a0b6211 100644
--- a/src/components/structures/FilePanel.js
+++ b/src/components/structures/FilePanel.js
@@ -26,10 +26,12 @@ import { _t } from '../../languageHandler';
 import BaseCard from "../views/right_panel/BaseCard";
 import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
 import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 /*
  * Component which shows the filtered file using a TimelinePanel
  */
+@replaceableComponent("structures.FilePanel")
 class FilePanel extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/structures/GenericErrorPage.js b/src/components/structures/GenericErrorPage.js
index ab7d4f9311..cfd2016d47 100644
--- a/src/components/structures/GenericErrorPage.js
+++ b/src/components/structures/GenericErrorPage.js
@@ -16,7 +16,9 @@ limitations under the License.
 
 import React from 'react';
 import PropTypes from 'prop-types';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.GenericErrorPage")
 export default class GenericErrorPage extends React.PureComponent {
     static propTypes = {
         title: PropTypes.object.isRequired, // jsx for title
diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js
index 96aa1ba728..976b2d81a5 100644
--- a/src/components/structures/GroupFilterPanel.js
+++ b/src/components/structures/GroupFilterPanel.js
@@ -30,7 +30,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
 import AutoHideScrollbar from "./AutoHideScrollbar";
 import SettingsStore from "../../settings/SettingsStore";
 import UserTagTile from "../views/elements/UserTagTile";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.GroupFilterPanel")
 class GroupFilterPanel extends React.Component {
     static contextType = MatrixClientContext;
 
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index bbc4187298..b4b871a0b4 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -39,6 +39,7 @@ import {Group} from "matrix-js-sdk";
 import {allSettled, sleep} from "../../utils/promise";
 import RightPanelStore from "../../stores/RightPanelStore";
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const LONG_DESC_PLACEHOLDER = _td(
 `<h1>HTML for your community's page</h1>
@@ -391,6 +392,7 @@ class FeaturedUser extends React.Component {
 const GROUP_JOINPOLICY_OPEN = "open";
 const GROUP_JOINPOLICY_INVITE = "invite";
 
+@replaceableComponent("structures.GroupView")
 export default class GroupView extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx
index 9cf84a9379..769775d549 100644
--- a/src/components/structures/HostSignupAction.tsx
+++ b/src/components/structures/HostSignupAction.tsx
@@ -22,11 +22,13 @@ import {
 import { _t } from "../../languageHandler";
 import { HostSignupStore } from "../../stores/HostSignupStore";
 import SdkConfig from "../../SdkConfig";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {}
 
 interface IState {}
 
+@replaceableComponent("structures.HostSignupAction")
 export default class HostSignupAction extends React.PureComponent<IProps, IState> {
     private openDialog = async () => {
         await HostSignupStore.instance.setHostSignupActive(true);
diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js
index cd5510de9d..341ab2df71 100644
--- a/src/components/structures/IndicatorScrollbar.js
+++ b/src/components/structures/IndicatorScrollbar.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from "react";
 import PropTypes from "prop-types";
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.IndicatorScrollbar")
 export default class IndicatorScrollbar extends React.Component {
     static propTypes = {
         // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js
index ac7049ed88..9b61f71fd7 100644
--- a/src/components/structures/InteractiveAuth.js
+++ b/src/components/structures/InteractiveAuth.js
@@ -22,9 +22,11 @@ import PropTypes from 'prop-types';
 import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
 
 import * as sdk from '../../index';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
 
+@replaceableComponent("structures.InteractiveAuthComponent")
 export default class InteractiveAuthComponent extends React.Component {
     static propTypes = {
         // matrix client to use for UI auth requests
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index 82dd9443cc..88c7a71b35 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -40,6 +40,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
 import RoomListNumResults from "../views/rooms/RoomListNumResults";
 import LeftPanelWidget from "./LeftPanelWidget";
 import SpacePanel from "../views/spaces/SpacePanel";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
     isMinimized: boolean;
@@ -60,6 +61,7 @@ const cssClasses = [
     "mx_RoomSublist_showNButton",
 ];
 
+@replaceableComponent("structures.LeftPanel")
 export default class LeftPanel extends React.Component<IProps, IState> {
     private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
     private groupFilterPanelWatcherRef: string;
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 3e6d56fd54..15e90a383a 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -56,6 +56,7 @@ import Modal from "../../Modal";
 import { ICollapseConfig } from "../../resizer/distributors/collapse";
 import HostSignupContainer from '../views/host_signup/HostSignupContainer';
 import { IOpts } from "../../createRoom";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 // We need to fetch each pinned message individually (if we don't already have it)
 // so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -128,6 +129,7 @@ interface IState {
  *
  * Components mounted below us can access the matrix client via the react context.
  */
+@replaceableComponent("structures.LoggedInView")
 class LoggedInView extends React.Component<IProps, IState> {
     static displayName = 'LoggedInView';
 
diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js
index 47dfe83ad6..5818d303fc 100644
--- a/src/components/structures/MainSplit.js
+++ b/src/components/structures/MainSplit.js
@@ -17,7 +17,9 @@ limitations under the License.
 
 import React from 'react';
 import { Resizable } from 're-resizable';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.MainSplit")
 export default class MainSplit extends React.Component {
     _onResizeStart = () => {
         this.props.resizeNotifier.startResizing();
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 1700b627db..0272633e8f 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -84,6 +84,7 @@ import DialPadModal from "../views/voip/DialPadModal";
 import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
 import SpaceStore from "../../stores/SpaceStore";
 import SpaceRoomDirectory from "./SpaceRoomDirectory";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 /** constants for MatrixChat.state.view */
 export enum Views {
@@ -208,6 +209,7 @@ interface IState {
     roomJustCreatedOpts?: IOpts;
 }
 
+@replaceableComponent("structures.MatrixChat")
 export default class MatrixChat extends React.PureComponent<IProps, IState> {
     static displayName = "MatrixChat";
 
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 9deda54bee..0f9ef70ec1 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -34,6 +34,7 @@ import {textForEvent} from "../../TextForEvent";
 import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
 import DMRoomMap from "../../utils/DMRoomMap";
 import NewRoomIntro from "../views/rooms/NewRoomIntro";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
 const continuedTypes = ['m.sticker', 'm.room.message'];
@@ -66,6 +67,7 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType()
 
 /* (almost) stateless UI component which builds the event tiles in the room timeline.
  */
+@replaceableComponent("structures.MessagePanel")
 export default class MessagePanel extends React.Component {
     static propTypes = {
         // true to give the component a 'display: none' style.
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index e0551eecdb..2ab11dad25 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -24,7 +24,9 @@ import dis from '../../dispatcher/dispatcher';
 import AccessibleButton from '../views/elements/AccessibleButton';
 import MatrixClientContext from "../../contexts/MatrixClientContext";
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.MyGroups")
 export default class MyGroups extends React.Component {
     static contextType = MatrixClientContext;
 
diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx
index 8d415df4dd..7c193ec9d7 100644
--- a/src/components/structures/NonUrgentToastContainer.tsx
+++ b/src/components/structures/NonUrgentToastContainer.tsx
@@ -18,6 +18,7 @@ import * as React from "react";
 import { ComponentClass } from "../../@types/common";
 import NonUrgentToastStore from "../../stores/NonUrgentToastStore";
 import { UPDATE_EVENT } from "../../stores/AsyncStore";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -26,6 +27,7 @@ interface IState {
     toasts: ComponentClass[],
 }
 
+@replaceableComponent("structures.NonUrgentToastContainer")
 export default class NonUrgentToastContainer extends React.PureComponent<IProps, IState> {
     public constructor(props, context) {
         super(props, context);
diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js
index b4eb6c187b..41aafc8b13 100644
--- a/src/components/structures/NotificationPanel.js
+++ b/src/components/structures/NotificationPanel.js
@@ -23,10 +23,12 @@ import { _t } from '../../languageHandler';
 import {MatrixClientPeg} from "../../MatrixClientPeg";
 import * as sdk from "../../index";
 import BaseCard from "../views/right_panel/BaseCard";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 /*
  * Component which shows the global notification list using a TimelinePanel
  */
+@replaceableComponent("structures.NotificationPanel")
 class NotificationPanel extends React.Component {
     static propTypes = {
         onClose: PropTypes.func.isRequired,
diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index 3d9df2e927..5bcb3b2450 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -34,7 +34,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
 import {Action} from "../../dispatcher/actions";
 import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
 import WidgetCard from "../views/right_panel/WidgetCard";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.RightPanel")
 export default class RightPanel extends React.Component {
     static get propTypes() {
         return {
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 7387e1aac0..363c67262b 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -34,6 +34,7 @@ import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
 import GroupStore from "../../stores/GroupStore";
 import FlairStore from "../../stores/FlairStore";
 import CountlyAnalytics from "../../CountlyAnalytics";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const MAX_NAME_LENGTH = 80;
 const MAX_TOPIC_LENGTH = 800;
@@ -42,6 +43,7 @@ function track(action) {
     Analytics.trackEvent('RoomDirectory', action);
 }
 
+@replaceableComponent("structures.RoomDirectory")
 export default class RoomDirectory extends React.Component {
     static propTypes = {
         initialText: PropTypes.string,
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index a64e40bc65..fda09f9774 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -25,6 +25,7 @@ import AccessibleButton from "../views/elements/AccessibleButton";
 import { Action } from "../../dispatcher/actions";
 import RoomListStore from "../../stores/room-list/RoomListStore";
 import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
     isMinimized: boolean;
@@ -37,6 +38,7 @@ interface IState {
     focused: boolean;
 }
 
+@replaceableComponent("structures.RoomSearch")
 export default class RoomSearch extends React.PureComponent<IProps, IState> {
     private dispatcherRef: string;
     private inputRef: React.RefObject<HTMLInputElement> = createRef();
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index aa4bceba74..8b70998be0 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -23,6 +23,7 @@ import Resend from '../../Resend';
 import dis from '../../dispatcher/dispatcher';
 import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
 import {Action} from "../../dispatcher/actions";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const STATUS_BAR_HIDDEN = 0;
 const STATUS_BAR_EXPANDED = 1;
@@ -35,6 +36,7 @@ function getUnsentMessages(room) {
     });
 }
 
+@replaceableComponent("structures.RoomStatusBar")
 export default class RoomStatusBar extends React.Component {
     static propTypes = {
         // the room this statusbar is representing.
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 90f6daf6cb..b57638413b 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -82,6 +82,7 @@ import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutS
 import { objectHasDiff } from "../../utils/objects";
 import SpaceRoomView from "./SpaceRoomView";
 import { IOpts } from "../../createRoom";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const DEBUG = false;
 let debuglog = function(msg: string) {};
@@ -195,6 +196,7 @@ export interface IState {
     dragCounter: number;
 }
 
+@replaceableComponent("structures.RoomView")
 export default class RoomView extends React.Component<IProps, IState> {
     private readonly dispatcherRef: string;
     private readonly roomStoreToken: EventSubscription;
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index 744400df3c..3a9b2b8a77 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
 import { Key } from '../../Keyboard';
 import Timer from '../../utils/Timer';
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const DEBUG_SCROLL = false;
 
@@ -83,6 +84,7 @@ if (DEBUG_SCROLL) {
  * offset as normal.
  */
 
+@replaceableComponent("structures.ScrollPanel")
 export default class ScrollPanel extends React.Component {
     static propTypes = {
         /* stickyBottom: if set to true, then once the user hits the bottom of
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js
index c1e3ad0cf2..6daa8526bc 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.js
@@ -22,7 +22,9 @@ import dis from '../../dispatcher/dispatcher';
 import {throttle} from 'lodash';
 import AccessibleButton from '../../components/views/elements/AccessibleButton';
 import classNames from 'classnames';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.SearchBox")
 export default class SearchBox extends React.Component {
     static propTypes = {
         onSearch: PropTypes.func,
diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx
index 21f9f3f5d6..0097d55cf5 100644
--- a/src/components/structures/TabbedView.tsx
+++ b/src/components/structures/TabbedView.tsx
@@ -20,6 +20,7 @@ import * as React from "react";
 import {_t} from '../../languageHandler';
 import * as sdk from "../../index";
 import AutoHideScrollbar from './AutoHideScrollbar';
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 /**
  * Represents a tab for the TabbedView.
@@ -45,6 +46,7 @@ interface IState {
     activeTabIndex: number;
 }
 
+@replaceableComponent("structures.TabbedView")
 export default class TabbedView extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 6bc1f70ba1..f32b8ed0a9 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -37,6 +37,7 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
 import {haveTileForEvent} from "../views/rooms/EventTile";
 import {UIFeature} from "../../settings/UIFeature";
 import {objectHasDiff} from "../../utils/objects";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const PAGINATE_SIZE = 20;
 const INITIAL_SIZE = 20;
@@ -55,6 +56,7 @@ if (DEBUG) {
  *
  * Also responsible for handling and sending read receipts.
  */
+@replaceableComponent("structures.TimelinePanel")
 class TimelinePanel extends React.Component {
     static propTypes = {
         // The js-sdk EventTimelineSet object for the timeline sequence we are
diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx
index 513cca82c3..1fd3e3419f 100644
--- a/src/components/structures/ToastContainer.tsx
+++ b/src/components/structures/ToastContainer.tsx
@@ -17,12 +17,14 @@ limitations under the License.
 import * as React from "react";
 import ToastStore, {IToast} from "../../stores/ToastStore";
 import classNames from "classnames";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IState {
     toasts: IToast<any>[];
     countSeen: number;
 }
 
+@replaceableComponent("structures.ToastContainer")
 export default class ToastContainer extends React.Component<{}, IState> {
     constructor(props, context) {
         super(props, context);
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index b9d157ee00..4a1fd4313d 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -25,6 +25,7 @@ import { Action } from "../../dispatcher/actions";
 import ProgressBar from "../views/elements/ProgressBar";
 import AccessibleButton from "../views/elements/AccessibleButton";
 import { IUpload } from "../../models/IUpload";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
     room: Room;
@@ -35,6 +36,7 @@ interface IState {
     uploadsHere: IUpload[];
 }
 
+@replaceableComponent("structures.UploadBar")
 export default class UploadBar extends React.Component<IProps, IState> {
     private dispatcherRef: string;
     private mounted: boolean;
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index b31a5f4b8e..0543cc4d07 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -56,6 +56,7 @@ import HostSignupAction from "./HostSignupAction";
 import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
 import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
 import RoomName from "../views/elements/RoomName";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 interface IProps {
     isMinimized: boolean;
@@ -69,6 +70,7 @@ interface IState {
     selectedSpace?: Room;
 }
 
+@replaceableComponent("structures.UserMenu")
 export default class UserMenu extends React.Component<IProps, IState> {
     private dispatcherRef: string;
     private themeWatcherRef: string;
diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js
index 8e21771bb9..dc05193ece 100644
--- a/src/components/structures/UserView.js
+++ b/src/components/structures/UserView.js
@@ -23,7 +23,9 @@ import * as sdk from "../../index";
 import Modal from '../../Modal';
 import { _t } from '../../languageHandler';
 import HomePage from "./HomePage";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
+@replaceableComponent("structures.UserView")
 export default class UserView extends React.Component {
     static get propTypes() {
         return {
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index 0b969784e5..704a1e7275 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -21,8 +21,10 @@ import PropTypes from 'prop-types';
 import SyntaxHighlight from '../views/elements/SyntaxHighlight';
 import {_t} from "../../languageHandler";
 import * as sdk from "../../index";
+import {replaceableComponent} from "../../utils/replaceableComponent";
 
 
+@replaceableComponent("structures.ViewSource")
 export default class ViewSource extends React.Component {
     static propTypes = {
         content: PropTypes.object.isRequired,
diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js
index c73691611d..eee5667052 100644
--- a/src/components/structures/auth/CompleteSecurity.js
+++ b/src/components/structures/auth/CompleteSecurity.js
@@ -26,7 +26,9 @@ import {
     PHASE_CONFIRM_SKIP,
 } from '../../../stores/SetupEncryptionStore';
 import SetupEncryptionBody from "./SetupEncryptionBody";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("structures.auth.CompleteSecurity")
 export default class CompleteSecurity extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/structures/auth/E2eSetup.js b/src/components/structures/auth/E2eSetup.js
index d97a972718..4e51ae828c 100644
--- a/src/components/structures/auth/E2eSetup.js
+++ b/src/components/structures/auth/E2eSetup.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import AuthPage from '../../views/auth/AuthPage';
 import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
 import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("structures.auth.E2eSetup")
 export default class E2eSetup extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js
index 5a39fe9fd9..31a5de0222 100644
--- a/src/components/structures/auth/ForgotPassword.js
+++ b/src/components/structures/auth/ForgotPassword.js
@@ -27,6 +27,7 @@ import classNames from 'classnames';
 import AuthPage from "../../views/auth/AuthPage";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import ServerPicker from "../../views/elements/ServerPicker";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // Phases
 // Show the forgot password inputs
@@ -38,6 +39,7 @@ const PHASE_EMAIL_SENT = 3;
 // User has clicked the link in email and completed reset
 const PHASE_DONE = 4;
 
+@replaceableComponent("structures.auth.ForgotPassword")
 export default class ForgotPassword extends React.Component {
     static propTypes = {
         serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 96fc39a437..3ab73fb9ac 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -35,6 +35,7 @@ import InlineSpinner from "../../views/elements/InlineSpinner";
 import Spinner from "../../views/elements/Spinner";
 import SSOButtons from "../../views/elements/SSOButtons";
 import ServerPicker from "../../views/elements/ServerPicker";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // 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.
@@ -99,6 +100,7 @@ interface IState {
 /*
  * A wire component which glues together login UI components and Login logic
  */
+@replaceableComponent("structures.auth.LoginComponent")
 export default class LoginComponent extends React.PureComponent<IProps, IState> {
     private unmounted = false;
     private loginLogic: Login;
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index f9d338902c..32bdddb82a 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -30,6 +30,7 @@ import Login, {ISSOFlow} from "../../../Login";
 import dis from "../../../dispatcher/dispatcher";
 import SSOButtons from "../../views/elements/SSOButtons";
 import ServerPicker from '../../views/elements/ServerPicker';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     serverConfig: ValidatedServerConfig;
@@ -109,6 +110,7 @@ interface IState {
     ssoFlow?: ISSOFlow;
 }
 
+@replaceableComponent("structures.auth.Registration")
 export default class Registration extends React.Component<IProps, IState> {
     loginLogic: Login;
 
diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js
index 3e7264dfec..f66c434edd 100644
--- a/src/components/structures/auth/SetupEncryptionBody.js
+++ b/src/components/structures/auth/SetupEncryptionBody.js
@@ -28,6 +28,7 @@ import {
     PHASE_CONFIRM_SKIP,
     PHASE_FINISHED,
 } from '../../../stores/SetupEncryptionStore';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function keyHasPassphrase(keyInfo) {
     return (
@@ -37,6 +38,7 @@ function keyHasPassphrase(keyInfo) {
     );
 }
 
+@replaceableComponent("structures.auth.SetupEncryptionBody")
 export default class SetupEncryptionBody extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js
index a7fe340457..08db3b2efe 100644
--- a/src/components/structures/auth/SoftLogout.js
+++ b/src/components/structures/auth/SoftLogout.js
@@ -26,6 +26,7 @@ import {sendLoginRequest} from "../../../Login";
 import AuthPage from "../../views/auth/AuthPage";
 import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
 import SSOButtons from "../../views/elements/SSOButtons";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const LOGIN_VIEW = {
     LOADING: 1,
@@ -41,6 +42,7 @@ const FLOWS_TO_VIEWS = {
     "m.login.sso": LOGIN_VIEW.SSO,
 };
 
+@replaceableComponent("structures.auth.SoftLogout")
 export default class SoftLogout extends React.Component {
     static propTypes = {
         // Query parameters from MatrixChat

From 3e189d2728393bb9b77cd55609679abd1bceba49 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 19:45:39 -0700
Subject: [PATCH 339/389] Batch of views getting replaceableComponent
 decorators

---
 src/components/views/auth/AuthBody.js                     | 2 ++
 src/components/views/auth/AuthFooter.js                   | 2 ++
 src/components/views/auth/AuthHeader.js                   | 2 ++
 src/components/views/auth/AuthHeaderLogo.js               | 2 ++
 src/components/views/auth/CaptchaForm.js                  | 2 ++
 src/components/views/auth/CompleteSecurityBody.js         | 2 ++
 src/components/views/auth/CountryDropdown.js              | 2 ++
 .../views/auth/InteractiveAuthEntryComponents.js          | 8 ++++++++
 src/components/views/auth/PassphraseField.tsx             | 2 ++
 src/components/views/auth/PasswordLogin.tsx               | 2 ++
 src/components/views/auth/RegistrationForm.tsx            | 2 ++
 src/components/views/auth/Welcome.js                      | 2 ++
 src/components/views/avatars/DecoratedRoomAvatar.tsx      | 2 ++
 src/components/views/avatars/GroupAvatar.tsx              | 2 ++
 src/components/views/avatars/MemberAvatar.tsx             | 2 ++
 src/components/views/avatars/MemberStatusMessageAvatar.js | 2 ++
 src/components/views/avatars/RoomAvatar.tsx               | 2 ++
 src/components/views/context_menus/CallContextMenu.tsx    | 2 ++
 src/components/views/context_menus/DialpadContextMenu.tsx | 2 ++
 .../views/context_menus/GenericElementContextMenu.js      | 2 ++
 .../views/context_menus/GenericTextContextMenu.js         | 2 ++
 .../views/context_menus/GroupInviteTileContextMenu.js     | 2 ++
 src/components/views/context_menus/MessageContextMenu.js  | 2 ++
 .../views/context_menus/StatusMessageContextMenu.js       | 2 ++
 src/components/views/context_menus/TagTileContextMenu.js  | 2 ++
 src/components/views/dialogs/AddressPickerDialog.js       | 3 ++-
 src/components/views/dialogs/AskInviteAnywayDialog.js     | 2 ++
 src/components/views/dialogs/BaseDialog.js                | 2 ++
 src/components/views/dialogs/BugReportDialog.js           | 2 ++
 .../views/dialogs/CommunityPrototypeInviteDialog.tsx      | 2 ++
 .../views/dialogs/ConfirmAndWaitRedactDialog.js           | 2 ++
 src/components/views/dialogs/ConfirmRedactDialog.js       | 2 ++
 src/components/views/dialogs/ConfirmUserActionDialog.js   | 2 ++
 .../dialogs/security/ConfirmDestroyCrossSigningDialog.js  | 2 ++
 .../views/dialogs/security/CreateCrossSigningDialog.js    | 2 ++
 .../views/dialogs/security/SetupEncryptionDialog.js       | 2 ++
 36 files changed, 78 insertions(+), 1 deletion(-)

diff --git a/src/components/views/auth/AuthBody.js b/src/components/views/auth/AuthBody.js
index 9a078efb52..fa7ad2b285 100644
--- a/src/components/views/auth/AuthBody.js
+++ b/src/components/views/auth/AuthBody.js
@@ -17,7 +17,9 @@ limitations under the License.
 'use strict';
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.AuthBody")
 export default class AuthBody extends React.PureComponent {
     render() {
         return <div className="mx_AuthBody">
diff --git a/src/components/views/auth/AuthFooter.js b/src/components/views/auth/AuthFooter.js
index 3de5a19350..f167e16283 100644
--- a/src/components/views/auth/AuthFooter.js
+++ b/src/components/views/auth/AuthFooter.js
@@ -18,7 +18,9 @@ limitations under the License.
 
 import { _t } from '../../../languageHandler';
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.AuthFooter")
 export default class AuthFooter extends React.Component {
     render() {
         return (
diff --git a/src/components/views/auth/AuthHeader.js b/src/components/views/auth/AuthHeader.js
index 57499e397c..323299b3a8 100644
--- a/src/components/views/auth/AuthHeader.js
+++ b/src/components/views/auth/AuthHeader.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.AuthHeader")
 export default class AuthHeader extends React.Component {
     static propTypes = {
         disableLanguageSelector: PropTypes.bool,
diff --git a/src/components/views/auth/AuthHeaderLogo.js b/src/components/views/auth/AuthHeaderLogo.js
index 9edf149a83..ea649893c6 100644
--- a/src/components/views/auth/AuthHeaderLogo.js
+++ b/src/components/views/auth/AuthHeaderLogo.js
@@ -17,7 +17,9 @@ limitations under the License.
 'use strict';
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.AuthHeaderLogo")
 export default class AuthHeaderLogo extends React.PureComponent {
     render() {
         return <div className="mx_AuthHeaderLogo">
diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js
index e2d7d594fa..50de24d403 100644
--- a/src/components/views/auth/CaptchaForm.js
+++ b/src/components/views/auth/CaptchaForm.js
@@ -18,12 +18,14 @@ import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const DIV_ID = 'mx_recaptcha';
 
 /**
  * A pure UI component which displays a captcha form.
  */
+@replaceableComponent("views.auth.CaptchaForm")
 export default class CaptchaForm extends React.Component {
     static propTypes = {
         sitePublicKey: PropTypes.string,
diff --git a/src/components/views/auth/CompleteSecurityBody.js b/src/components/views/auth/CompleteSecurityBody.js
index d757de9fe0..91cd66f150 100644
--- a/src/components/views/auth/CompleteSecurityBody.js
+++ b/src/components/views/auth/CompleteSecurityBody.js
@@ -17,7 +17,9 @@ limitations under the License.
 'use strict';
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.auth.CompleteSecurityBody")
 export default class CompleteSecurityBody extends React.PureComponent {
     render() {
         return <div className="mx_CompleteSecurityBody">
diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.js
index 3296b574a4..e21f112865 100644
--- a/src/components/views/auth/CountryDropdown.js
+++ b/src/components/views/auth/CountryDropdown.js
@@ -22,6 +22,7 @@ import * as sdk from '../../../index';
 import {COUNTRIES, getEmojiFlag} from '../../../phonenumber';
 import SdkConfig from "../../../SdkConfig";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const COUNTRIES_BY_ISO2 = {};
 for (const c of COUNTRIES) {
@@ -40,6 +41,7 @@ function countryMatchesSearchQuery(query, country) {
     return false;
 }
 
+@replaceableComponent("views.auth.CountryDropdown")
 export default class CountryDropdown extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js
index 7dc1976641..6cbecd22ee 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.js
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.js
@@ -26,6 +26,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import AccessibleButton from "../elements/AccessibleButton";
 import Spinner from "../elements/Spinner";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /* This file contains a collection of components which are used by the
  * InteractiveAuth to prompt the user to enter the information needed
@@ -75,6 +76,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics";
 
 export const DEFAULT_PHASE = 0;
 
+@replaceableComponent("views.auth.PasswordAuthEntry")
 export class PasswordAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.password";
 
@@ -173,6 +175,7 @@ export class PasswordAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.RecaptchaAuthEntry")
 export class RecaptchaAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.recaptcha";
 
@@ -235,6 +238,7 @@ export class RecaptchaAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.TermsAuthEntry")
 export class TermsAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.terms";
 
@@ -385,6 +389,7 @@ export class TermsAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.EmailIdentityAuthEntry")
 export class EmailIdentityAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.email.identity";
 
@@ -432,6 +437,7 @@ export class EmailIdentityAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.MsisdnAuthEntry")
 export class MsisdnAuthEntry extends React.Component {
     static LOGIN_TYPE = "m.login.msisdn";
 
@@ -578,6 +584,7 @@ export class MsisdnAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.SSOAuthEntry")
 export class SSOAuthEntry extends React.Component {
     static propTypes = {
         matrixClient: PropTypes.object.isRequired,
@@ -708,6 +715,7 @@ export class SSOAuthEntry extends React.Component {
     }
 }
 
+@replaceableComponent("views.auth.FallbackAuthEntry")
 export class FallbackAuthEntry extends React.Component {
     static propTypes = {
         matrixClient: PropTypes.object.isRequired,
diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx
index e240ad61ca..274c244b2a 100644
--- a/src/components/views/auth/PassphraseField.tsx
+++ b/src/components/views/auth/PassphraseField.tsx
@@ -22,6 +22,7 @@ import SdkConfig from "../../../SdkConfig";
 import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
 import {_t, _td} from "../../../languageHandler";
 import Field, {IInputProps} from "../elements/Field";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends Omit<IInputProps, "onValidate"> {
     autoFocus?: boolean;
@@ -40,6 +41,7 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
     onValidate(result: IValidationResult);
 }
 
+@replaceableComponent("views.auth.PassphraseField")
 class PassphraseField extends PureComponent<IProps> {
     static defaultProps = {
         label: _td("Password"),
diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx
index b2a3d62f55..2a42804a61 100644
--- a/src/components/views/auth/PasswordLogin.tsx
+++ b/src/components/views/auth/PasswordLogin.tsx
@@ -26,6 +26,7 @@ import withValidation from "../elements/Validation";
 import * as Email from "../../../email";
 import Field from "../elements/Field";
 import CountryDropdown from "./CountryDropdown";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // For validating phone numbers without country codes
 const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@@ -66,6 +67,7 @@ enum LoginField {
  * A pure UI component which displays a username/password form.
  * The email/username/phone fields are fully-controlled, the password field is not.
  */
+@replaceableComponent("views.auth.PasswordLogin")
 export default class PasswordLogin extends React.PureComponent<IProps, IState> {
     static defaultProps = {
         onUsernameChanged: function() {},
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index e42ed88f99..85e0933be9 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -30,6 +30,7 @@ import PassphraseField from "./PassphraseField";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import Field from '../elements/Field';
 import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 enum RegistrationField {
     Email = "field_email",
@@ -80,6 +81,7 @@ interface IState {
 /*
  * A pure UI component which displays a registration form.
  */
+@replaceableComponent("views.auth.RegistrationForm")
 export default class RegistrationForm extends React.PureComponent<IProps, IState> {
     static defaultProps = {
         onValidationChange: console.error,
diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js
index 0205f4e0b9..fca66fcf9b 100644
--- a/src/components/views/auth/Welcome.js
+++ b/src/components/views/auth/Welcome.js
@@ -24,10 +24,12 @@ import {_td} from "../../../languageHandler";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // translatable strings for Welcome pages
 _td("Sign in with SSO");
 
+@replaceableComponent("views.auth.Welcome")
 export default class Welcome extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx
index d7e012467b..e95022687a 100644
--- a/src/components/views/avatars/DecoratedRoomAvatar.tsx
+++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx
@@ -30,6 +30,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {_t} from "../../../languageHandler";
 import TextWithTooltip from "../elements/TextWithTooltip";
 import DMRoomMap from "../../../utils/DMRoomMap";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     room: Room;
@@ -68,6 +69,7 @@ function tooltipText(variant: Icon) {
     }
 }
 
+@replaceableComponent("views.avatars.DecoratedRoomAvatar")
 export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
     private _dmUser: User;
     private isUnmounted = false;
diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx
index 51327605c0..a033257871 100644
--- a/src/components/views/avatars/GroupAvatar.tsx
+++ b/src/components/views/avatars/GroupAvatar.tsx
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import BaseAvatar from './BaseAvatar';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export interface IProps {
         groupId?: string;
@@ -28,6 +29,7 @@ export interface IProps {
         onClick?: React.MouseEventHandler;
 }
 
+@replaceableComponent("views.avatars.GroupAvatar")
 export default class GroupAvatar extends React.Component<IProps> {
     public static defaultProps = {
         width: 36,
diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index 60b043016b..641046aa55 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -22,6 +22,7 @@ import dis from "../../../dispatcher/dispatcher";
 import {Action} from "../../../dispatcher/actions";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import BaseAvatar from "./BaseAvatar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
     member: RoomMember;
@@ -42,6 +43,7 @@ interface IState {
     imageUrl?: string;
 }
 
+@replaceableComponent("views.avatars.MemberAvatar")
 export default class MemberAvatar extends React.Component<IProps, IState> {
     public static defaultProps = {
         width: 40,
diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js
index d5d927106c..acf190f17f 100644
--- a/src/components/views/avatars/MemberStatusMessageAvatar.js
+++ b/src/components/views/avatars/MemberStatusMessageAvatar.js
@@ -23,7 +23,9 @@ import classNames from 'classnames';
 import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
 import SettingsStore from "../../../settings/SettingsStore";
 import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
 export default class MemberStatusMessageAvatar extends React.Component {
     static propTypes = {
         member: PropTypes.object.isRequired,
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 952b9d4cb6..0a59f6e36a 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -23,6 +23,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import Modal from '../../../Modal';
 import * as Avatar from '../../../Avatar';
 import {ResizeMethod} from "../../../Avatar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
     // Room may be left unset here, but if it is,
@@ -42,6 +43,7 @@ interface IState {
     urls: string[];
 }
 
+@replaceableComponent("views.avatars.RoomAvatar")
 export default class RoomAvatar extends React.Component<IProps, IState> {
     public static defaultProps = {
         width: 36,
diff --git a/src/components/views/context_menus/CallContextMenu.tsx b/src/components/views/context_menus/CallContextMenu.tsx
index 3557976326..97473059a6 100644
--- a/src/components/views/context_menus/CallContextMenu.tsx
+++ b/src/components/views/context_menus/CallContextMenu.tsx
@@ -22,11 +22,13 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import CallHandler from '../../../CallHandler';
 import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';
 import Modal from '../../../Modal';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IContextMenuProps {
     call: MatrixCall;
 }
 
+@replaceableComponent("views.context_menus.CallContextMenu")
 export default class CallContextMenu extends React.Component<IProps> {
     static propTypes = {
         // js-sdk User object. Not required because it might not exist.
diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx
index e3aed0179b..17abce0c61 100644
--- a/src/components/views/context_menus/DialpadContextMenu.tsx
+++ b/src/components/views/context_menus/DialpadContextMenu.tsx
@@ -19,6 +19,7 @@ import { _t } from '../../../languageHandler';
 import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
 import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import Dialpad from '../voip/DialPad';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IContextMenuProps {
     call: MatrixCall;
@@ -28,6 +29,7 @@ interface IState {
     value: string;
 }
 
+@replaceableComponent("views.context_menus.DialpadContextMenu")
 export default class DialpadContextMenu extends React.Component<IProps, IState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/context_menus/GenericElementContextMenu.js b/src/components/views/context_menus/GenericElementContextMenu.js
index cea684b663..e04e3f7695 100644
--- a/src/components/views/context_menus/GenericElementContextMenu.js
+++ b/src/components/views/context_menus/GenericElementContextMenu.js
@@ -16,6 +16,7 @@ limitations under the License.
 
 import React from 'react';
 import PropTypes from 'prop-types';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * This component can be used to display generic HTML content in a contextual
@@ -23,6 +24,7 @@ import PropTypes from 'prop-types';
  */
 
 
+@replaceableComponent("views.context_menus.GenericElementContextMenu")
 export default class GenericElementContextMenu extends React.Component {
     static propTypes = {
         element: PropTypes.element.isRequired,
diff --git a/src/components/views/context_menus/GenericTextContextMenu.js b/src/components/views/context_menus/GenericTextContextMenu.js
index 068f83be5f..3d3add006f 100644
--- a/src/components/views/context_menus/GenericTextContextMenu.js
+++ b/src/components/views/context_menus/GenericTextContextMenu.js
@@ -16,7 +16,9 @@ limitations under the License.
 
 import React from 'react';
 import PropTypes from 'prop-types';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.context_menus.GenericTextContextMenu")
 export default class GenericTextContextMenu extends React.Component {
     static propTypes = {
         message: PropTypes.string.isRequired,
diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js
index 27ef76452f..11a9d90ac2 100644
--- a/src/components/views/context_menus/GroupInviteTileContextMenu.js
+++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js
@@ -23,7 +23,9 @@ import Modal from '../../../Modal';
 import {Group} from 'matrix-js-sdk';
 import GroupStore from "../../../stores/GroupStore";
 import {MenuItem} from "../../structures/ContextMenu";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.context_menus.GroupInviteTileContextMenu")
 export default class GroupInviteTileContextMenu extends React.Component {
     static propTypes = {
         group: PropTypes.instanceOf(Group).isRequired,
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index 6b871e4f24..e19cfab809 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -32,11 +32,13 @@ import { isUrlPermitted } from '../../../HtmlUtils';
 import { isContentActionable } from '../../../utils/EventUtils';
 import {MenuItem} from "../../structures/ContextMenu";
 import {EventType} from "matrix-js-sdk/src/@types/event";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function canCancel(eventStatus) {
     return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
 }
 
+@replaceableComponent("views.context_menus.MessageContextMenu")
 export default class MessageContextMenu extends React.Component {
     static propTypes = {
         /* the MatrixEvent associated with the context menu */
diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js
index 5e6f06dd5d..41f0e0ba61 100644
--- a/src/components/views/context_menus/StatusMessageContextMenu.js
+++ b/src/components/views/context_menus/StatusMessageContextMenu.js
@@ -20,7 +20,9 @@ import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import AccessibleButton from '../elements/AccessibleButton';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.context_menus.StatusMessageContextMenu")
 export default class StatusMessageContextMenu extends React.Component {
     static propTypes = {
         // js-sdk User object. Not required because it might not exist.
diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js
index 8d690483a8..8dea62690c 100644
--- a/src/components/views/context_menus/TagTileContextMenu.js
+++ b/src/components/views/context_menus/TagTileContextMenu.js
@@ -22,7 +22,9 @@ import dis from '../../../dispatcher/dispatcher';
 import TagOrderActions from '../../../actions/TagOrderActions';
 import {MenuItem} from "../../structures/ContextMenu";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.context_menus.TagTileContextMenu")
 export default class TagTileContextMenu extends React.Component {
     static propTypes = {
         tag: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js
index 2cd09874b2..929d688e47 100644
--- a/src/components/views/dialogs/AddressPickerDialog.js
+++ b/src/components/views/dialogs/AddressPickerDialog.js
@@ -33,6 +33,7 @@ import { abbreviateUrl } from '../../../utils/UrlUtils';
 import {sleep} from "../../../utils/promise";
 import {Key} from "../../../Keyboard";
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const TRUNCATE_QUERY_LIST = 40;
 const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@@ -43,7 +44,7 @@ const addressTypeName = {
     'email': _td("email address"),
 };
 
-
+@replaceableComponent("views.dialogs.AddressPickerDialog")
 export default class AddressPickerDialog extends React.Component {
     static propTypes = {
         title: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.js
index c69400977a..e6cd45ba6b 100644
--- a/src/components/views/dialogs/AskInviteAnywayDialog.js
+++ b/src/components/views/dialogs/AskInviteAnywayDialog.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import {SettingLevel} from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
 export default class AskInviteAnywayDialog extends React.Component {
     static propTypes = {
         unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index 9ba5368ee5..0858e53e50 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -26,6 +26,7 @@ import AccessibleButton from '../elements/AccessibleButton';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { _t } from "../../../languageHandler";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * Basic container for modal dialogs.
@@ -33,6 +34,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
  * Includes a div for the title, and a keypress handler which cancels the
  * dialog on escape.
  */
+@replaceableComponent("views.dialogs.BaseDialog")
 export default class BaseDialog extends React.Component {
     static propTypes = {
         // onFinished callback to call when Escape is pressed
diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js
index c4dd0a1430..8948c14c7c 100644
--- a/src/components/views/dialogs/BugReportDialog.js
+++ b/src/components/views/dialogs/BugReportDialog.js
@@ -25,7 +25,9 @@ import Modal from '../../../Modal';
 import { _t } from '../../../languageHandler';
 import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-rageshake';
 import AccessibleButton from "../elements/AccessibleButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.BugReportDialog")
 export default class BugReportDialog extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
index 1c8a4ad6f6..d1080566ac 100644
--- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
+++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
@@ -31,6 +31,7 @@ import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
 import StyledCheckbox from "../elements/StyledCheckbox";
 import Modal from "../../../Modal";
 import ErrorDialog from "./ErrorDialog";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IDialogProps {
     roomId: string;
@@ -52,6 +53,7 @@ interface IState {
     busy: boolean;
 }
 
+@replaceableComponent("views.dialogs.CommunityPrototypeInviteDialog")
 export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
index 0622dd7dfb..37d5510756 100644
--- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
+++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * A dialog for confirming a redaction.
@@ -30,6 +31,7 @@ import { _t } from '../../../languageHandler';
  *
  * To avoid this, we keep the dialog open as long as /redact is in progress.
  */
+@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
 export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.js
index 2216f9a93a..bd63d3acc1 100644
--- a/src/components/views/dialogs/ConfirmRedactDialog.js
+++ b/src/components/views/dialogs/ConfirmRedactDialog.js
@@ -17,10 +17,12 @@ limitations under the License.
 import React from 'react';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * A dialog for confirming a redaction.
  */
+@replaceableComponent("views.dialogs.ConfirmRedactDialog")
 export default class ConfirmRedactDialog extends React.Component {
     render() {
         const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js
index 44f57f047e..8827f161f1 100644
--- a/src/components/views/dialogs/ConfirmUserActionDialog.js
+++ b/src/components/views/dialogs/ConfirmUserActionDialog.js
@@ -20,6 +20,7 @@ import { MatrixClient } from 'matrix-js-sdk';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import { GroupMemberType } from '../../../groups';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * A dialog for confirming an operation on another user.
@@ -29,6 +30,7 @@ import { GroupMemberType } from '../../../groups';
  * to make it obvious what is going to happen.
  * Also tweaks the style for 'dangerous' actions (albeit only with colour)
  */
+@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
 export default class ConfirmUserActionDialog extends React.Component {
     static propTypes = {
         // matrix-js-sdk (room) member object. Supply either this or 'groupMember'
diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js
index abc1586205..43fb25f152 100644
--- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js
+++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import {_t} from "../../../../languageHandler";
 import * as sdk from "../../../../index";
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
 export default class ConfirmDestroyCrossSigningDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.js b/src/components/views/dialogs/security/CreateCrossSigningDialog.js
index be546d2616..fedcc02f89 100644
--- a/src/components/views/dialogs/security/CreateCrossSigningDialog.js
+++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.js
@@ -25,12 +25,14 @@ import DialogButtons from '../../elements/DialogButtons';
 import BaseDialog from '../BaseDialog';
 import Spinner from '../../elements/Spinner';
 import InteractiveAuthDialog from '../InteractiveAuthDialog';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
  * Walks the user through the process of creating a cross-signing keys. In most
  * cases, only a spinner is shown, but for more complex auth like SSO, the user
  * may need to complete some steps to proceed.
  */
+@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
 export default class CreateCrossSigningDialog extends React.PureComponent {
     static propTypes = {
         accountPassword: PropTypes.string,
diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.js b/src/components/views/dialogs/security/SetupEncryptionDialog.js
index 9ce3144534..3c15ea9f1d 100644
--- a/src/components/views/dialogs/security/SetupEncryptionDialog.js
+++ b/src/components/views/dialogs/security/SetupEncryptionDialog.js
@@ -20,6 +20,7 @@ import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
 import BaseDialog from '../BaseDialog';
 import { _t } from '../../../../languageHandler';
 import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 function iconFromPhase(phase) {
     if (phase === PHASE_DONE) {
@@ -29,6 +30,7 @@ function iconFromPhase(phase) {
     }
 }
 
+@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
 export default class SetupEncryptionDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,

From c359dff73879ca231a7bbc970ff532a6973ef293 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 19:59:41 -0700
Subject: [PATCH 340/389] Batch of views getting replaceableComponent
 decorators

---
 src/components/views/dialogs/ConfirmWipeDeviceDialog.js        | 2 ++
 .../views/dialogs/CreateCommunityPrototypeDialog.tsx           | 2 ++
 src/components/views/dialogs/CreateGroupDialog.js              | 2 ++
 src/components/views/dialogs/CreateRoomDialog.js               | 2 ++
 src/components/views/dialogs/DeactivateAccountDialog.js        | 2 ++
 src/components/views/dialogs/DevtoolsDialog.js                 | 2 ++
 src/components/views/dialogs/EditCommunityPrototypeDialog.tsx  | 2 ++
 src/components/views/dialogs/ErrorDialog.js                    | 2 ++
 src/components/views/dialogs/HostSignupDialog.tsx              | 2 ++
 src/components/views/dialogs/IncomingSasDialog.js              | 2 ++
 src/components/views/dialogs/IntegrationsDisabledDialog.js     | 2 ++
 src/components/views/dialogs/IntegrationsImpossibleDialog.js   | 2 ++
 src/components/views/dialogs/InteractiveAuthDialog.js          | 2 ++
 src/components/views/dialogs/InviteDialog.tsx                  | 2 ++
 src/components/views/dialogs/LogoutDialog.js                   | 2 ++
 .../views/dialogs/ManualDeviceKeyVerificationDialog.js         | 2 ++
 src/components/views/dialogs/MessageEditHistoryDialog.js       | 2 ++
 src/components/views/dialogs/ModalWidgetDialog.tsx             | 2 ++
 src/components/views/dialogs/ReportEventDialog.js              | 2 ++
 src/components/views/dialogs/RoomSettingsDialog.js             | 2 ++
 src/components/views/dialogs/RoomUpgradeDialog.js              | 2 ++
 src/components/views/dialogs/RoomUpgradeWarningDialog.js       | 2 ++
 src/components/views/dialogs/ServerOfflineDialog.tsx           | 2 ++
 src/components/views/dialogs/ServerPickerDialog.tsx            | 2 ++
 src/components/views/dialogs/SessionRestoreErrorDialog.js      | 3 ++-
 src/components/views/dialogs/SetEmailDialog.js                 | 2 ++
 src/components/views/dialogs/ShareDialog.tsx                   | 2 ++
 src/components/views/dialogs/StorageEvictedDialog.js           | 2 ++
 src/components/views/dialogs/TabbedIntegrationManagerDialog.js | 2 ++
 src/components/views/dialogs/TermsDialog.js                    | 2 ++
 src/components/views/dialogs/TextInputDialog.js                | 2 ++
 src/components/views/dialogs/UploadConfirmDialog.js            | 2 ++
 src/components/views/dialogs/UploadFailureDialog.js            | 2 ++
 src/components/views/dialogs/UserSettingsDialog.js             | 2 ++
 src/components/views/dialogs/VerificationRequestDialog.js      | 2 ++
 .../views/dialogs/WidgetCapabilitiesPromptDialog.tsx           | 2 ++
 src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js  | 2 ++
 src/components/views/elements/AccessibleTooltipButton.tsx      | 2 ++
 src/components/views/elements/ActionButton.js                  | 2 ++
 src/components/views/elements/AddressSelector.js               | 2 ++
 src/components/views/elements/AddressTile.js                   | 3 ++-
 src/components/views/elements/AppPermission.js                 | 2 ++
 src/components/views/elements/AppTile.js                       | 2 ++
 src/components/views/elements/DesktopCapturerSourcePicker.tsx  | 2 ++
 src/components/views/elements/DialogButtons.js                 | 2 ++
 src/components/views/elements/DirectorySearchBox.js            | 2 ++
 src/components/views/elements/Draggable.tsx                    | 2 ++
 src/components/views/elements/Dropdown.js                      | 2 ++
 src/components/views/elements/EditableItemList.js              | 2 ++
 src/components/views/elements/EditableText.js                  | 2 ++
 src/components/views/elements/EditableTextContainer.js         | 2 ++
 src/components/views/elements/ErrorBoundary.js                 | 2 ++
 src/components/views/elements/EventTilePreview.tsx             | 2 ++
 src/components/views/elements/Flair.js                         | 2 ++
 src/components/views/elements/IRCTimelineProfileResizer.tsx    | 2 ++
 src/components/views/elements/ImageView.js                     | 2 ++
 src/components/views/elements/InfoTooltip.tsx                  | 2 ++
 src/components/views/elements/InlineSpinner.js                 | 2 ++
 src/components/views/elements/LabelledToggleSwitch.js          | 2 ++
 src/components/views/elements/LanguageDropdown.js              | 2 ++
 src/components/views/elements/LazyRenderList.js                | 2 ++
 src/components/views/elements/MemberEventListSummary.tsx       | 2 ++
 src/components/views/elements/PersistedElement.js              | 2 ++
 src/components/views/elements/PersistentApp.js                 | 2 ++
 src/components/views/elements/Pill.js                          | 2 ++
 src/components/views/elements/PowerSelector.js                 | 2 ++
 src/components/views/elements/ReplyThread.js                   | 2 ++
 src/components/views/elements/RoomAliasField.js                | 2 ++
 src/components/views/elements/SettingsFlag.tsx                 | 2 ++
 src/components/views/elements/Slider.tsx                       | 2 ++
 src/components/views/elements/SpellCheckLanguagesDropdown.tsx  | 2 ++
 src/components/views/elements/Spoiler.js                       | 2 ++
 src/components/views/elements/StyledCheckbox.tsx               | 2 ++
 src/components/views/elements/StyledRadioButton.tsx            | 2 ++
 src/components/views/elements/SyntaxHighlight.js               | 2 ++
 src/components/views/elements/TagTile.js                       | 2 ++
 src/components/views/elements/TextWithTooltip.js               | 2 ++
 src/components/views/elements/TintableSvg.js                   | 2 ++
 src/components/views/elements/Tooltip.tsx                      | 2 ++
 src/components/views/elements/TooltipButton.js                 | 2 ++
 src/components/views/elements/TruncatedList.js                 | 2 ++
 src/components/views/elements/UserTagTile.tsx                  | 2 ++
 82 files changed, 164 insertions(+), 2 deletions(-)

diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js
index 41ef9131fa..4faaad0f7e 100644
--- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js
+++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import {_t} from "../../../languageHandler";
 import * as sdk from "../../../index";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
 export default class ConfirmWipeDeviceDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
index 1d9d92b9c9..9b4484d661 100644
--- a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
+++ b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
@@ -25,6 +25,7 @@ import InfoTooltip from "../elements/InfoTooltip";
 import dis from "../../../dispatcher/dispatcher";
 import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
 import GroupStore from "../../../stores/GroupStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IDialogProps {
 }
@@ -38,6 +39,7 @@ interface IState {
     avatarPreview: string;
 }
 
+@replaceableComponent("views.dialogs.CreateCommunityPrototypeDialog")
 export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
     private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
 
diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js
index 6636153c98..e6c7a67aca 100644
--- a/src/components/views/dialogs/CreateGroupDialog.js
+++ b/src/components/views/dialogs/CreateGroupDialog.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.CreateGroupDialog")
 export default class CreateGroupDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js
index 0771b0ec45..e9dc6e2be0 100644
--- a/src/components/views/dialogs/CreateRoomDialog.js
+++ b/src/components/views/dialogs/CreateRoomDialog.js
@@ -27,7 +27,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import {Key} from "../../../Keyboard";
 import {privateShouldBeEncrypted} from "../../../createRoom";
 import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.CreateRoomDialog")
 export default class CreateRoomDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js
index fca8c42546..4e52549d51 100644
--- a/src/components/views/dialogs/DeactivateAccountDialog.js
+++ b/src/components/views/dialogs/DeactivateAccountDialog.js
@@ -26,7 +26,9 @@ import { _t } from '../../../languageHandler';
 import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
 import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
 import StyledCheckbox from "../elements/StyledCheckbox";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.DeactivateAccountDialog")
 export default class DeactivateAccountDialog extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index 814378bb51..9b24aaa571 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -38,6 +38,7 @@ import {SETTINGS} from "../../../settings/Settings";
 import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
 import Modal from "../../../Modal";
 import ErrorDialog from "./ErrorDialog";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class GenericEditor extends React.PureComponent {
     // static propTypes = {onBack: PropTypes.func.isRequired};
@@ -1089,6 +1090,7 @@ const Entries = [
     SettingsExplorer,
 ];
 
+@replaceableComponent("views.dialogs.DevtoolsDialog")
 export default class DevtoolsDialog extends React.PureComponent {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
index 3071854b3e..504d563bd9 100644
--- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
+++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
@@ -23,6 +23,7 @@ import AccessibleButton from "../elements/AccessibleButton";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
 import FlairStore from "../../../stores/FlairStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IDialogProps {
     communityId: string;
@@ -38,6 +39,7 @@ interface IState {
 }
 
 // XXX: This is a lot of duplication from the create dialog, just in a different shape
+@replaceableComponent("views.dialogs.EditCommunityPrototypeDialog")
 export default class EditCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
     private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
 
diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js
index 3bfa635adf..5197c68b5a 100644
--- a/src/components/views/dialogs/ErrorDialog.js
+++ b/src/components/views/dialogs/ErrorDialog.js
@@ -29,7 +29,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.ErrorDialog")
 export default class ErrorDialog extends React.Component {
     static propTypes = {
         title: PropTypes.string,
diff --git a/src/components/views/dialogs/HostSignupDialog.tsx b/src/components/views/dialogs/HostSignupDialog.tsx
index 45a03b7cf0..c8bc907136 100644
--- a/src/components/views/dialogs/HostSignupDialog.tsx
+++ b/src/components/views/dialogs/HostSignupDialog.tsx
@@ -31,6 +31,7 @@ import {
     IPostmessageResponseData,
     PostmessageAction,
 } from "./HostSignupDialogTypes";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const HOST_SIGNUP_KEY = "host_signup";
 
@@ -42,6 +43,7 @@ interface IState {
     minimized: boolean;
 }
 
+@replaceableComponent("views.dialogs.HostSignupDialog")
 export default class HostSignupDialog extends React.PureComponent<IProps, IState> {
     private iframeRef: React.RefObject<HTMLIFrameElement> = React.createRef();
     private readonly config: IHostSignupConfig;
diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js
index 2a4ff9cec3..d65ec7563f 100644
--- a/src/components/views/dialogs/IncomingSasDialog.js
+++ b/src/components/views/dialogs/IncomingSasDialog.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const PHASE_START = 0;
 const PHASE_SHOW_SAS = 1;
@@ -26,6 +27,7 @@ const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 2;
 const PHASE_VERIFIED = 3;
 const PHASE_CANCELLED = 4;
 
+@replaceableComponent("views.dialogs.IncomingSasDialog")
 export default class IncomingSasDialog extends React.Component {
     static propTypes = {
         verifier: PropTypes.object.isRequired,
diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.js b/src/components/views/dialogs/IntegrationsDisabledDialog.js
index 7c996fbeab..0e9878f4bc 100644
--- a/src/components/views/dialogs/IntegrationsDisabledDialog.js
+++ b/src/components/views/dialogs/IntegrationsDisabledDialog.js
@@ -20,7 +20,9 @@ import {_t} from "../../../languageHandler";
 import * as sdk from "../../../index";
 import dis from '../../../dispatcher/dispatcher';
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.IntegrationsDisabledDialog")
 export default class IntegrationsDisabledDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js
index 68bedc711d..9bc9d02ba6 100644
--- a/src/components/views/dialogs/IntegrationsImpossibleDialog.js
+++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import {_t} from "../../../languageHandler";
 import SdkConfig from "../../../SdkConfig";
 import * as sdk from "../../../index";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.IntegrationsImpossibleDialog")
 export default class IntegrationsImpossibleDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js
index 22291225ad..28a9bf673a 100644
--- a/src/components/views/dialogs/InteractiveAuthDialog.js
+++ b/src/components/views/dialogs/InteractiveAuthDialog.js
@@ -25,7 +25,9 @@ import { _t } from '../../../languageHandler';
 import AccessibleButton from '../elements/AccessibleButton';
 import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
 import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.InteractiveAuthDialog")
 export default class InteractiveAuthDialog extends React.Component {
     static propTypes = {
         // matrix client to use for UI auth requests
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 9bc5b6476f..db9077291e 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -42,6 +42,7 @@ import {UIFeature} from "../../../settings/UIFeature";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import {Room} from "matrix-js-sdk/src/models/room";
 import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
 /* eslint-disable camelcase */
@@ -337,6 +338,7 @@ interface IInviteDialogState {
     errorText: string,
 }
 
+@replaceableComponent("views.dialogs.InviteDialog")
 export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
     static defaultProps = {
         kind: KIND_DM,
diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js
index af36dba2b6..7bced46d43 100644
--- a/src/components/views/dialogs/LogoutDialog.js
+++ b/src/components/views/dialogs/LogoutDialog.js
@@ -22,7 +22,9 @@ import dis from '../../../dispatcher/dispatcher';
 import { _t } from '../../../languageHandler';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
 import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.LogoutDialog")
 export default class LogoutDialog extends React.Component {
     defaultProps = {
         onFinished: function() {},
diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js
index 4b9d7239e6..3151edd796 100644
--- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js
+++ b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.js
@@ -24,7 +24,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import * as FormattingUtils from '../../../utils/FormattingUtils';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.ManualDeviceKeyVerificationDialog")
 export default class ManualDeviceKeyVerificationDialog extends React.Component {
     static propTypes = {
         userId: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js
index 2bdf2be35c..7585561c0c 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.js
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.js
@@ -21,7 +21,9 @@ import { _t } from '../../../languageHandler';
 import * as sdk from "../../../index";
 import {wantsDateSeparator} from '../../../DateUtils';
 import SettingsStore from '../../../settings/SettingsStore';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.MessageEditHistoryDialog")
 export default class MessageEditHistoryDialog extends React.PureComponent {
     static propTypes = {
         mxEvent: PropTypes.object.isRequired,
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index 92fb406965..59eaab7b81 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -38,6 +38,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {OwnProfileStore} from "../../../stores/OwnProfileStore";
 import { arrayFastClone } from "../../../utils/arrays";
 import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     widgetDefinition: IModalWidgetOpenRequestData;
@@ -53,6 +54,7 @@ interface IState {
 
 const MAX_BUTTONS = 3;
 
+@replaceableComponent("views.dialogs.ModalWidgetDialog")
 export default class ModalWidgetDialog extends React.PureComponent<IProps, IState> {
     private readonly widget: Widget;
     private readonly possibleButtons: ModalButtonID[];
diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js
index f5509dec4d..67ed0f8f53 100644
--- a/src/components/views/dialogs/ReportEventDialog.js
+++ b/src/components/views/dialogs/ReportEventDialog.js
@@ -22,10 +22,12 @@ import {MatrixEvent} from "matrix-js-sdk";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import SdkConfig from '../../../SdkConfig';
 import Markdown from '../../../Markdown';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * A dialog for reporting an event.
  */
+@replaceableComponent("views.dialogs.ReportEventDialog")
 export default class ReportEventDialog extends PureComponent {
     static propTypes = {
         mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js
index 9d9313f08f..045d11404a 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.js
+++ b/src/components/views/dialogs/RoomSettingsDialog.js
@@ -30,6 +30,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import dis from "../../../dispatcher/dispatcher";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
 export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
@@ -38,6 +39,7 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
 export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
 export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
 
+@replaceableComponent("views.dialogs.RoomSettingsDialog")
 export default class RoomSettingsDialog extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js
index 85e97444ed..8f9ed42ada 100644
--- a/src/components/views/dialogs/RoomUpgradeDialog.js
+++ b/src/components/views/dialogs/RoomUpgradeDialog.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import Modal from '../../../Modal';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.RoomUpgradeDialog")
 export default class RoomUpgradeDialog extends React.Component {
     static propTypes = {
         room: PropTypes.object.isRequired,
diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.js b/src/components/views/dialogs/RoomUpgradeWarningDialog.js
index c83528c5ba..452ac56dff 100644
--- a/src/components/views/dialogs/RoomUpgradeWarningDialog.js
+++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.js
@@ -22,7 +22,9 @@ import * as sdk from "../../../index";
 import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import Modal from "../../../Modal";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.RoomUpgradeWarningDialog")
 export default class RoomUpgradeWarningDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx
index 81f628343b..52ff056907 100644
--- a/src/components/views/dialogs/ServerOfflineDialog.tsx
+++ b/src/components/views/dialogs/ServerOfflineDialog.tsx
@@ -28,10 +28,12 @@ import AccessibleButton from "../elements/AccessibleButton";
 import { UPDATE_EVENT } from "../../../stores/AsyncStore";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { IDialogProps } from "./IDialogProps";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends IDialogProps {
 }
 
+@replaceableComponent("views.dialogs.ServerOfflineDialog")
 export default class ServerOfflineDialog extends React.PureComponent<IProps> {
     public componentDidMount() {
         EchoStore.instance.on(UPDATE_EVENT, this.onEchosUpdated);
diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index 7ca115760e..4abc0a88b1 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -26,6 +26,7 @@ import Field from "../elements/Field";
 import StyledRadioButton from "../elements/StyledRadioButton";
 import TextWithTooltip from "../elements/TextWithTooltip";
 import withValidation, {IFieldState} from "../elements/Validation";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     title?: string;
@@ -38,6 +39,7 @@ interface IState {
     otherHomeserver: string;
 }
 
+@replaceableComponent("views.dialogs.ServerPickerDialog")
 export default class ServerPickerDialog extends React.PureComponent<IProps, IState> {
     private readonly defaultServer: ValidatedServerConfig;
     private readonly fieldRef = createRef<Field>();
diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js
index bae6b19fbe..50d7fbea09 100644
--- a/src/components/views/dialogs/SessionRestoreErrorDialog.js
+++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js
@@ -22,8 +22,9 @@ import * as sdk from '../../../index';
 import SdkConfig from '../../../SdkConfig';
 import Modal from '../../../Modal';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.dialogs.SessionRestoreErrorDialog")
 export default class SessionRestoreErrorDialog extends React.Component {
     static propTypes = {
         error: PropTypes.string.isRequired,
diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js
index 6514d94dc9..0f8f410a6a 100644
--- a/src/components/views/dialogs/SetEmailDialog.js
+++ b/src/components/views/dialogs/SetEmailDialog.js
@@ -22,6 +22,7 @@ import * as Email from '../../../email';
 import AddThreepid from '../../../AddThreepid';
 import { _t } from '../../../languageHandler';
 import Modal from '../../../Modal';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 
 /*
@@ -29,6 +30,7 @@ import Modal from '../../../Modal';
  *
  * On success, `onFinished(true)` is called.
  */
+@replaceableComponent("views.dialogs.SetEmailDialog")
 export default class SetEmailDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx
index 5264031cc6..df1206a4f0 100644
--- a/src/components/views/dialogs/ShareDialog.tsx
+++ b/src/components/views/dialogs/ShareDialog.tsx
@@ -34,6 +34,7 @@ import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
 import { IDialogProps } from "./IDialogProps";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const socials = [
     {
@@ -73,6 +74,7 @@ interface IState {
     permalinkCreator: RoomPermalinkCreator;
 }
 
+@replaceableComponent("views.dialogs.ShareDialog")
 export default class ShareDialog extends React.PureComponent<IProps, IState> {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/StorageEvictedDialog.js b/src/components/views/dialogs/StorageEvictedDialog.js
index a22f302807..15c5347644 100644
--- a/src/components/views/dialogs/StorageEvictedDialog.js
+++ b/src/components/views/dialogs/StorageEvictedDialog.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import SdkConfig from '../../../SdkConfig';
 import Modal from '../../../Modal';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.StorageEvictedDialog")
 export default class StorageEvictedDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
index 9f5c9f6a11..07e29adcff 100644
--- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
+++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
@@ -22,7 +22,9 @@ import * as sdk from '../../../index';
 import {dialogTermsInteractionCallback, TermsNotSignedError} from "../../../Terms";
 import classNames from 'classnames';
 import * as ScalarMessaging from "../../../ScalarMessaging";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.TabbedIntegrationManagerDialog")
 export default class TabbedIntegrationManagerDialog extends React.Component {
     static propTypes = {
         /**
diff --git a/src/components/views/dialogs/TermsDialog.js b/src/components/views/dialogs/TermsDialog.js
index 402605c545..72e6c3f3a0 100644
--- a/src/components/views/dialogs/TermsDialog.js
+++ b/src/components/views/dialogs/TermsDialog.js
@@ -21,6 +21,7 @@ import * as sdk from '../../../index';
 import { _t, pickBestLanguage } from '../../../languageHandler';
 
 import Matrix from 'matrix-js-sdk';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class TermsCheckbox extends React.PureComponent {
     static propTypes = {
@@ -41,6 +42,7 @@ class TermsCheckbox extends React.PureComponent {
     }
 }
 
+@replaceableComponent("views.dialogs.TermsDialog")
 export default class TermsDialog extends React.PureComponent {
     static propTypes = {
         /**
diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js
index 69cc4390be..97abd209c0 100644
--- a/src/components/views/dialogs/TextInputDialog.js
+++ b/src/components/views/dialogs/TextInputDialog.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import Field from "../elements/Field";
 import { _t, _td } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.TextInputDialog")
 export default class TextInputDialog extends React.Component {
     static propTypes = {
         title: PropTypes.string,
diff --git a/src/components/views/dialogs/UploadConfirmDialog.js b/src/components/views/dialogs/UploadConfirmDialog.js
index e3521eb282..2ff16b9440 100644
--- a/src/components/views/dialogs/UploadConfirmDialog.js
+++ b/src/components/views/dialogs/UploadConfirmDialog.js
@@ -20,7 +20,9 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import filesize from "filesize";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.UploadConfirmDialog")
 export default class UploadConfirmDialog extends React.Component {
     static propTypes = {
         file: PropTypes.object.isRequired,
diff --git a/src/components/views/dialogs/UploadFailureDialog.js b/src/components/views/dialogs/UploadFailureDialog.js
index 4be1656f66..d220d6c684 100644
--- a/src/components/views/dialogs/UploadFailureDialog.js
+++ b/src/components/views/dialogs/UploadFailureDialog.js
@@ -21,12 +21,14 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import ContentMessages from '../../../ContentMessages';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /*
  * Tells the user about files we know cannot be uploaded before we even try uploading
  * them. This is named fairly generically but the only thing we check right now is
  * the size of the file.
  */
+@replaceableComponent("views.dialogs.UploadFailureDialog")
 export default class UploadFailureDialog extends React.Component {
     static propTypes = {
         badFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js
index 7164540aea..32abef874b 100644
--- a/src/components/views/dialogs/UserSettingsDialog.js
+++ b/src/components/views/dialogs/UserSettingsDialog.js
@@ -33,6 +33,7 @@ import * as sdk from "../../../index";
 import SdkConfig from "../../../SdkConfig";
 import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
 export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
@@ -45,6 +46,7 @@ export const USER_LABS_TAB = "USER_LABS_TAB";
 export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
 export const USER_HELP_TAB = "USER_HELP_TAB";
 
+@replaceableComponent("views.dialogs.UserSettingsDialog")
 export default class UserSettingsDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.js
index 3a6e9a2d10..574beebbc6 100644
--- a/src/components/views/dialogs/VerificationRequestDialog.js
+++ b/src/components/views/dialogs/VerificationRequestDialog.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.VerificationRequestDialog")
 export default class VerificationRequestDialog extends React.Component {
     static propTypes = {
         verificationRequest: PropTypes.object,
diff --git a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
index 535e0b7b8e..70fe7fe5e3 100644
--- a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
+++ b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
@@ -29,6 +29,7 @@ import StyledCheckbox from "../elements/StyledCheckbox";
 import DialogButtons from "../elements/DialogButtons";
 import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import { CapabilityText } from "../../../widgets/CapabilityText";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] {
     return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]");
@@ -54,6 +55,7 @@ interface IState {
     rememberSelection: boolean;
 }
 
+@replaceableComponent("views.dialogs.WidgetCapabilitiesPromptDialog")
 export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<IProps, IState> {
     private eventPermissionsMap = new Map<Capability, WidgetEventCapability>();
 
diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
index c01d3d39b8..f45adf9738 100644
--- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
+++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
@@ -21,7 +21,9 @@ import * as sdk from "../../../index";
 import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import {Widget} from "matrix-widget-api";
 import {OIDCState, WidgetPermissionStore} from "../../../stores/widgets/WidgetPermissionStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.dialogs.WidgetOpenIDPermissionsDialog")
 export default class WidgetOpenIDPermissionsDialog extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func.isRequired,
diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx
index b7c7b78e63..3bb264fb3e 100644
--- a/src/components/views/elements/AccessibleTooltipButton.tsx
+++ b/src/components/views/elements/AccessibleTooltipButton.tsx
@@ -20,6 +20,7 @@ import classNames from 'classnames';
 
 import AccessibleButton from "./AccessibleButton";
 import Tooltip from './Tooltip';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
     title: string;
@@ -33,6 +34,7 @@ interface IState {
     hover: boolean;
 }
 
+@replaceableComponent("views.elements.AccessibleTooltipButton")
 export default class AccessibleTooltipButton extends React.PureComponent<ITooltipProps, IState> {
     constructor(props: ITooltipProps) {
         super(props);
diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js
index bec016bce0..1714891cb5 100644
--- a/src/components/views/elements/ActionButton.js
+++ b/src/components/views/elements/ActionButton.js
@@ -20,7 +20,9 @@ import AccessibleButton from './AccessibleButton';
 import dis from '../../../dispatcher/dispatcher';
 import * as sdk from '../../../index';
 import Analytics from '../../../Analytics';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.ActionButton")
 export default class ActionButton extends React.Component {
     static propTypes = {
         size: PropTypes.string,
diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js
index 2a71622bb8..33b2906870 100644
--- a/src/components/views/elements/AddressSelector.js
+++ b/src/components/views/elements/AddressSelector.js
@@ -20,7 +20,9 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import classNames from 'classnames';
 import { UserAddressType } from '../../../UserAddress';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.AddressSelector")
 export default class AddressSelector extends React.Component {
     static propTypes = {
         onSelected: PropTypes.func.isRequired,
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js
index dc6c6b2914..4a216dbae4 100644
--- a/src/components/views/elements/AddressTile.js
+++ b/src/components/views/elements/AddressTile.js
@@ -22,8 +22,9 @@ import * as sdk from "../../../index";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import { _t } from '../../../languageHandler';
 import { UserAddressType } from '../../../UserAddress.js';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.elements.AddressTile")
 export default class AddressTile extends React.Component {
     static propTypes = {
         address: UserAddressType.isRequired,
diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js
index ec8bffc32f..65e40ef19a 100644
--- a/src/components/views/elements/AppPermission.js
+++ b/src/components/views/elements/AppPermission.js
@@ -24,7 +24,9 @@ import { _t } from '../../../languageHandler';
 import SdkConfig from '../../../SdkConfig';
 import WidgetUtils from "../../../utils/WidgetUtils";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.AppPermission")
 export default class AppPermission extends React.Component {
     static propTypes = {
         url: PropTypes.string.isRequired,
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 2a72621ccc..e206fda797 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -38,7 +38,9 @@ import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions
 import {MatrixCapabilities} from "matrix-widget-api";
 import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
 import WidgetAvatar from "../avatars/WidgetAvatar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.AppTile")
 export default class AppTile extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
index 6ae465c362..2d066a7ed7 100644
--- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx
+++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
@@ -19,6 +19,7 @@ import { _t } from '../../../languageHandler';
 import BaseDialog from "..//dialogs/BaseDialog"
 import AccessibleButton from './AccessibleButton';
 import {getDesktopCapturerSources} from "matrix-js-sdk/src/webrtc/call";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export interface DesktopCapturerSource {
     id: string;
@@ -69,6 +70,7 @@ export interface DesktopCapturerSourcePickerIProps {
     onFinished(source: DesktopCapturerSource): void;
 }
 
+@replaceableComponent("views.elements.DesktopCapturerSourcePicker")
 export default class DesktopCapturerSourcePicker extends React.Component<
     DesktopCapturerSourcePickerIProps,
     DesktopCapturerSourcePickerIState
diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js
index 3417485eb8..dcb1cee077 100644
--- a/src/components/views/elements/DialogButtons.js
+++ b/src/components/views/elements/DialogButtons.js
@@ -19,10 +19,12 @@ limitations under the License.
 import React from "react";
 import PropTypes from "prop-types";
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /**
  * Basic container for buttons in modal dialogs.
  */
+@replaceableComponent("views.elements.DialogButtons")
 export default class DialogButtons extends React.Component {
     static propTypes = {
         // The primary button which is styled differently and has default focus.
diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.js
index 644b69417b..6447bb3cd8 100644
--- a/src/components/views/elements/DirectorySearchBox.js
+++ b/src/components/views/elements/DirectorySearchBox.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.DirectorySearchBox")
 export default class DirectorySearchBox extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx
index a6eb8323f3..6032721a48 100644
--- a/src/components/views/elements/Draggable.tsx
+++ b/src/components/views/elements/Draggable.tsx
@@ -15,6 +15,7 @@ limitations under the License.
 */
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     className: string;
@@ -33,6 +34,7 @@ export interface ILocationState {
     currentY: number;
 }
 
+@replaceableComponent("views.elements.Draggable")
 export default class Draggable extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js
index 6b3efb5ee1..981c0becc0 100644
--- a/src/components/views/elements/Dropdown.js
+++ b/src/components/views/elements/Dropdown.js
@@ -22,6 +22,7 @@ import classnames from 'classnames';
 import AccessibleButton from './AccessibleButton';
 import { _t } from '../../../languageHandler';
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class MenuOption extends React.Component {
     constructor(props) {
@@ -83,6 +84,7 @@ MenuOption.propTypes = {
  *
  * TODO: Port NetworkDropdown to use this.
  */
+@replaceableComponent("views.elements.Dropdown")
 export default class Dropdown extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js
index 5a07a400d7..ff62f169fa 100644
--- a/src/components/views/elements/EditableItemList.js
+++ b/src/components/views/elements/EditableItemList.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
 import {_t} from '../../../languageHandler';
 import Field from "./Field";
 import AccessibleButton from "./AccessibleButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export class EditableItem extends React.Component {
     static propTypes = {
@@ -85,6 +86,7 @@ export class EditableItem extends React.Component {
     }
 }
 
+@replaceableComponent("views.elements.EditableItemList")
 export default class EditableItemList extends React.Component {
     static propTypes = {
         id: PropTypes.string.isRequired,
diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js
index 49eb331aef..638fd02553 100644
--- a/src/components/views/elements/EditableText.js
+++ b/src/components/views/elements/EditableText.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.EditableText")
 export default class EditableText extends React.Component {
     static propTypes = {
         onValueChanged: PropTypes.func,
diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js
index bbc5560557..e925220089 100644
--- a/src/components/views/elements/EditableTextContainer.js
+++ b/src/components/views/elements/EditableTextContainer.js
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /**
  * A component which wraps an EditableText, with a spinner while updates take
@@ -29,6 +30,7 @@ import * as sdk from '../../../index';
  * similarly asynchronous way. If this is not provided, the initial value is
  * taken from the 'initialValue' property.
  */
+@replaceableComponent("views.elements.EditableTextContainer")
 export default class EditableTextContainer extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js
index 9fe6861250..9037287f49 100644
--- a/src/components/views/elements/ErrorBoundary.js
+++ b/src/components/views/elements/ErrorBoundary.js
@@ -21,11 +21,13 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import PlatformPeg from '../../../PlatformPeg';
 import Modal from '../../../Modal';
 import SdkConfig from "../../../SdkConfig";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 /**
  * This error boundary component can be used to wrap large content areas and
  * catch exceptions during rendering in the component tree below them.
  */
+@replaceableComponent("views.elements.ErrorBoundary")
 export default class ErrorBoundary extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index 49c97831bc..c539f2be1c 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -24,6 +24,7 @@ import EventTile from '../rooms/EventTile';
 import SettingsStore from "../../../settings/SettingsStore";
 import {Layout} from "../../../settings/Layout";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     /**
@@ -52,6 +53,7 @@ interface IState {
 
 const AVATAR_SIZE = 32;
 
+@replaceableComponent("views.elements.EventTilePreview")
 export default class EventTilePreview extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js
index 0f06904b68..04eba5bc42 100644
--- a/src/components/views/elements/Flair.js
+++ b/src/components/views/elements/Flair.js
@@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
 import FlairStore from '../../../stores/FlairStore';
 import dis from '../../../dispatcher/dispatcher';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 
 class FlairAvatar extends React.Component {
@@ -64,6 +65,7 @@ FlairAvatar.propTypes = {
 
 FlairAvatar.contextType = MatrixClientContext;
 
+@replaceableComponent("views.elements.Flair")
 export default class Flair extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx
index ecd63816de..cd1ccf2fc4 100644
--- a/src/components/views/elements/IRCTimelineProfileResizer.tsx
+++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx
@@ -18,6 +18,7 @@ import React from 'react';
 import SettingsStore from "../../../settings/SettingsStore";
 import Draggable, {ILocationState} from './Draggable';
 import { SettingLevel } from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // Current room
@@ -31,6 +32,7 @@ interface IState {
     IRCLayoutRoot: HTMLElement;
 }
 
+@replaceableComponent("views.elements.IRCTimelineProfileResizer")
 export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js
index e39075cedc..96b6de832d 100644
--- a/src/components/views/elements/ImageView.js
+++ b/src/components/views/elements/ImageView.js
@@ -26,7 +26,9 @@ import Modal from "../../../Modal";
 import * as sdk from "../../../index";
 import {Key} from "../../../Keyboard";
 import FocusLock from "react-focus-lock";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.ImageView")
 export default class ImageView extends React.Component {
     static propTypes = {
         src: PropTypes.string.isRequired, // the source of the image being displayed
diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx
index dd21c95b74..8f7f1ea53f 100644
--- a/src/components/views/elements/InfoTooltip.tsx
+++ b/src/components/views/elements/InfoTooltip.tsx
@@ -20,6 +20,7 @@ import classNames from 'classnames';
 
 import Tooltip from './Tooltip';
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface ITooltipProps {
     tooltip?: React.ReactNode;
@@ -30,6 +31,7 @@ interface IState {
     hover: boolean;
 }
 
+@replaceableComponent("views.elements.InfoTooltip")
 export default class InfoTooltip extends React.PureComponent<ITooltipProps, IState> {
     constructor(props: ITooltipProps) {
         super(props);
diff --git a/src/components/views/elements/InlineSpinner.js b/src/components/views/elements/InlineSpinner.js
index 73316157f4..3654a1f34c 100644
--- a/src/components/views/elements/InlineSpinner.js
+++ b/src/components/views/elements/InlineSpinner.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from "react";
 import {_t} from "../../../languageHandler";
 import SettingsStore from "../../../settings/SettingsStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.InlineSpinner")
 export default class InlineSpinner extends React.Component {
     render() {
         const w = this.props.w || 16;
diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.js
index 78beb2aa91..e6378f0e6a 100644
--- a/src/components/views/elements/LabelledToggleSwitch.js
+++ b/src/components/views/elements/LabelledToggleSwitch.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from "prop-types";
 import ToggleSwitch from "./ToggleSwitch";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.LabelledToggleSwitch")
 export default class LabelledToggleSwitch extends React.Component {
     static propTypes = {
         // The value for the toggle switch
diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js
index 03ec456af5..2e961be700 100644
--- a/src/components/views/elements/LanguageDropdown.js
+++ b/src/components/views/elements/LanguageDropdown.js
@@ -22,6 +22,7 @@ import * as sdk from '../../../index';
 import * as languageHandler from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function languageMatchesSearchQuery(query, language) {
     if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@@ -29,6 +30,7 @@ function languageMatchesSearchQuery(query, language) {
     return false;
 }
 
+@replaceableComponent("views.elements.LanguageDropdown")
 export default class LanguageDropdown extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.js
index 7572dced0b..f2c8148cd2 100644
--- a/src/components/views/elements/LazyRenderList.js
+++ b/src/components/views/elements/LazyRenderList.js
@@ -16,6 +16,7 @@ limitations under the License.
 
 import React from "react";
 import PropTypes from 'prop-types';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class ItemRange {
     constructor(topCount, renderCount, bottomCount) {
@@ -55,6 +56,7 @@ class ItemRange {
     }
 }
 
+@replaceableComponent("views.elements.LazyRenderList")
 export default class LazyRenderList extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx
index 073bedf207..0290ef6d83 100644
--- a/src/components/views/elements/MemberEventListSummary.tsx
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -24,6 +24,7 @@ import { _t } from '../../../languageHandler';
 import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
 import { isValid3pidInvite } from "../../../RoomInvite";
 import EventListSummary from "./EventListSummary";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // An array of member events to summarise
@@ -69,6 +70,7 @@ enum TransitionType {
 
 const SEP = ",";
 
+@replaceableComponent("views.elements.MemberEventListSummary")
 export default class MemberEventListSummary extends React.Component<IProps> {
     static defaultProps = {
         summaryLength: 1,
diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js
index 3732f644b8..f504b3e97f 100644
--- a/src/components/views/elements/PersistedElement.js
+++ b/src/components/views/elements/PersistedElement.js
@@ -24,6 +24,7 @@ import dis from '../../../dispatcher/dispatcher';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // Shamelessly ripped off Modal.js.  There's probably a better way
 // of doing reusable widgets like dialog boxes & menus where we go and
@@ -56,6 +57,7 @@ function getOrCreateContainer(containerId) {
  * children are made visible and are positioned into a div that is given the same
  * bounding rect as the parent of PE.
  */
+@replaceableComponent("views.elements.PersistedElement")
 export default class PersistedElement extends React.Component {
     static propTypes = {
         // Unique identifier for this PersistedElement instance
diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js
index 7801076c66..5df373e4fe 100644
--- a/src/components/views/elements/PersistentApp.js
+++ b/src/components/views/elements/PersistentApp.js
@@ -21,7 +21,9 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
 import WidgetUtils from '../../../utils/WidgetUtils';
 import * as sdk from '../../../index';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.PersistentApp")
 export default class PersistentApp extends React.Component {
     state = {
         roomId: RoomViewStore.getRoomId(),
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index c6806c289e..b0d4fc7fa2 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -27,7 +27,9 @@ import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/perma
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {Action} from "../../../dispatcher/actions";
 import Tooltip from './Tooltip';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.Pill")
 class Pill extends React.Component {
     static roomNotifPos(text) {
         return text.indexOf("@room");
diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js
index 66922df0f8..622bed9890 100644
--- a/src/components/views/elements/PowerSelector.js
+++ b/src/components/views/elements/PowerSelector.js
@@ -20,7 +20,9 @@ import * as Roles from '../../../Roles';
 import { _t } from '../../../languageHandler';
 import Field from "./Field";
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.PowerSelector")
 export default class PowerSelector extends React.Component {
     static propTypes = {
         value: PropTypes.number.isRequired,
diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js
index 27d773b099..2e0cc50435 100644
--- a/src/components/views/elements/ReplyThread.js
+++ b/src/components/views/elements/ReplyThread.js
@@ -31,10 +31,12 @@ import {Action} from "../../../dispatcher/actions";
 import sanitizeHtml from "sanitize-html";
 import {UIFeature} from "../../../settings/UIFeature";
 import {PERMITTED_URL_SCHEMES} from "../../../HtmlUtils";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // This component does no cycle detection, simply because the only way to make such a cycle would be to
 // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
 // be low as each event being loaded (after the first) is triggered by an explicit user action.
+@replaceableComponent("views.elements.ReplyThread")
 export default class ReplyThread extends React.Component {
     static propTypes = {
         // the latest event in this chain of replies
diff --git a/src/components/views/elements/RoomAliasField.js b/src/components/views/elements/RoomAliasField.js
index 04bbe1c3de..1db154c2cd 100644
--- a/src/components/views/elements/RoomAliasField.js
+++ b/src/components/views/elements/RoomAliasField.js
@@ -19,8 +19,10 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import withValidation from './Validation';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // Controlled form component wrapping Field for inputting a room alias scoped to a given domain
+@replaceableComponent("views.elements.RoomAliasField")
 export default class RoomAliasField extends React.PureComponent {
     static propTypes = {
         domain: PropTypes.string.isRequired,
diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx
index 03e91fac62..4f885ab47d 100644
--- a/src/components/views/elements/SettingsFlag.tsx
+++ b/src/components/views/elements/SettingsFlag.tsx
@@ -21,6 +21,7 @@ import { _t } from '../../../languageHandler';
 import ToggleSwitch from "./ToggleSwitch";
 import StyledCheckbox from "./StyledCheckbox";
 import { SettingLevel } from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // The setting must be a boolean
@@ -39,6 +40,7 @@ interface IState {
     value: boolean;
 }
 
+@replaceableComponent("views.elements.SettingsFlag")
 export default class SettingsFlag extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx
index b7c8e1b533..b513f90460 100644
--- a/src/components/views/elements/Slider.tsx
+++ b/src/components/views/elements/Slider.tsx
@@ -15,6 +15,7 @@ limitations under the License.
 */
 
 import * as React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // A callback for the selected value
@@ -34,6 +35,7 @@ interface IProps {
     disabled: boolean;
 }
 
+@replaceableComponent("views.elements.Slider")
 export default class Slider extends React.Component<IProps> {
     // offset is a terrible inverse approximation.
     // if the values represents some function f(x) = y where x is the
diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
index c647f6e410..06e1efe415 100644
--- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -21,6 +21,7 @@ import * as sdk from '../../../index';
 import PlatformPeg from "../../../PlatformPeg";
 import SettingsStore from "../../../settings/SettingsStore";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function languageMatchesSearchQuery(query, language) {
     if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@@ -39,6 +40,7 @@ interface SpellCheckLanguagesDropdownIState {
     languages: any,
 }
 
+@replaceableComponent("views.elements.SpellCheckLanguagesDropdown")
 export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps,
                                                                          SpellCheckLanguagesDropdownIState> {
     constructor(props) {
diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.js
index b75967b225..33b4382a2c 100644
--- a/src/components/views/elements/Spoiler.js
+++ b/src/components/views/elements/Spoiler.js
@@ -15,7 +15,9 @@
  */
 
 import React from 'react';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.Spoiler")
 export default class Spoiler extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx
index f8d2665d07..2454d1336b 100644
--- a/src/components/views/elements/StyledCheckbox.tsx
+++ b/src/components/views/elements/StyledCheckbox.tsx
@@ -16,6 +16,7 @@ limitations under the License.
 
 import React from "react";
 import { randomString } from "matrix-js-sdk/src/randomstring";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
 }
@@ -23,6 +24,7 @@ interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
 interface IState {
 }
 
+@replaceableComponent("views.elements.StyledCheckbox")
 export default class StyledCheckbox extends React.PureComponent<IProps, IState> {
     private id: string;
 
diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx
index 2efd084861..835394e055 100644
--- a/src/components/views/elements/StyledRadioButton.tsx
+++ b/src/components/views/elements/StyledRadioButton.tsx
@@ -16,6 +16,7 @@ limitations under the License.
 
 import React from 'react';
 import classnames from 'classnames';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
     outlined?: boolean;
@@ -24,6 +25,7 @@ interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
 interface IState {
 }
 
+@replaceableComponent("views.elements.StyledRadioButton")
 export default class StyledRadioButton extends React.PureComponent<IProps, IState> {
     public static readonly defaultProps = {
         className: '',
diff --git a/src/components/views/elements/SyntaxHighlight.js b/src/components/views/elements/SyntaxHighlight.js
index a4dc97d46e..f9874c5367 100644
--- a/src/components/views/elements/SyntaxHighlight.js
+++ b/src/components/views/elements/SyntaxHighlight.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import {highlightBlock} from 'highlight.js';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.SyntaxHighlight")
 export default class SyntaxHighlight extends React.Component {
     static propTypes = {
         className: PropTypes.string,
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index 6c9a01a840..663acd6329 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -30,12 +30,14 @@ import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import AccessibleButton from "./AccessibleButton";
 import SettingsStore from "../../../settings/SettingsStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
 // a thing to click on for the user to filter the visible rooms in the RoomList to:
 //  - Rooms that are part of the group
 //  - Direct messages with members of the group
 // with the intention that this could be expanded to arbitrary tags in future.
+@replaceableComponent("views.elements.TagTile")
 export default class TagTile extends React.Component {
     static propTypes = {
         // A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js
index b0405dc4c9..e4ad234ae2 100644
--- a/src/components/views/elements/TextWithTooltip.js
+++ b/src/components/views/elements/TextWithTooltip.js
@@ -17,7 +17,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.TextWithTooltip")
 export default class TextWithTooltip extends React.Component {
     static propTypes = {
         class: PropTypes.string,
diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js
index df55b0a854..690deeedcd 100644
--- a/src/components/views/elements/TintableSvg.js
+++ b/src/components/views/elements/TintableSvg.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import Tinter from "../../../Tinter";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.TintableSvg")
 class TintableSvg extends React.Component {
     static propTypes = {
         src: PropTypes.string.isRequired,
diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx
index 03b9eb08d0..b2dd00de18 100644
--- a/src/components/views/elements/Tooltip.tsx
+++ b/src/components/views/elements/Tooltip.tsx
@@ -21,6 +21,7 @@ limitations under the License.
 import React, {Component, CSSProperties} from 'react';
 import ReactDOM from 'react-dom';
 import classNames from 'classnames';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const MIN_TOOLTIP_HEIGHT = 25;
 
@@ -39,6 +40,7 @@ interface IProps {
         yOffset?: number;
 }
 
+@replaceableComponent("views.elements.Tooltip")
 export default class Tooltip extends React.Component<IProps> {
     private tooltipContainer: HTMLElement;
     private tooltip: void | Element | Component<Element, any, any>;
diff --git a/src/components/views/elements/TooltipButton.js b/src/components/views/elements/TooltipButton.js
index 240d763bdc..c5ebb3b1aa 100644
--- a/src/components/views/elements/TooltipButton.js
+++ b/src/components/views/elements/TooltipButton.js
@@ -17,7 +17,9 @@ limitations under the License.
 
 import React from 'react';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.TooltipButton")
 export default class TooltipButton extends React.Component {
     state = {
         hover: false,
diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.js
index 81eb057e36..0509775545 100644
--- a/src/components/views/elements/TruncatedList.js
+++ b/src/components/views/elements/TruncatedList.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.elements.TruncatedList")
 export default class TruncatedList extends React.Component {
     static propTypes = {
         // The number of elements to show before truncating. If negative, no truncation is done.
diff --git a/src/components/views/elements/UserTagTile.tsx b/src/components/views/elements/UserTagTile.tsx
index e7c74bb10e..d3e07a0a34 100644
--- a/src/components/views/elements/UserTagTile.tsx
+++ b/src/components/views/elements/UserTagTile.tsx
@@ -21,6 +21,7 @@ import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
 import AccessibleTooltipButton from "./AccessibleTooltipButton";
 import classNames from "classnames";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -29,6 +30,7 @@ interface IState {
     selected: boolean;
 }
 
+@replaceableComponent("views.elements.UserTagTile")
 export default class UserTagTile extends React.PureComponent<IProps, IState> {
     private tagStoreRef: fbEmitter.EventSubscription;
 

From fc5b1ed9d686f2d297f44872eb47812a5648dea3 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 20:04:46 -0700
Subject: [PATCH 341/389] Batch of views getting replaceableComponent
 decorators

---
 src/components/views/emojipicker/Category.tsx               | 2 ++
 src/components/views/emojipicker/Emoji.tsx                  | 2 ++
 src/components/views/emojipicker/EmojiPicker.tsx            | 2 ++
 src/components/views/emojipicker/Header.tsx                 | 2 ++
 src/components/views/emojipicker/Preview.tsx                | 2 ++
 src/components/views/emojipicker/QuickReactions.tsx         | 2 ++
 src/components/views/emojipicker/ReactionPicker.tsx         | 2 ++
 src/components/views/emojipicker/Search.tsx                 | 2 ++
 src/components/views/groups/GroupInviteTile.js              | 2 ++
 src/components/views/groups/GroupMemberList.js              | 2 ++
 src/components/views/groups/GroupMemberTile.js              | 2 ++
 src/components/views/groups/GroupPublicityToggle.js         | 2 ++
 src/components/views/groups/GroupRoomInfo.js                | 2 ++
 src/components/views/groups/GroupRoomList.js                | 2 ++
 src/components/views/groups/GroupRoomTile.js                | 2 ++
 src/components/views/groups/GroupTile.js                    | 2 ++
 src/components/views/groups/GroupUserSettings.js            | 2 ++
 src/components/views/messages/DateSeparator.js              | 2 ++
 src/components/views/messages/EditHistoryMessage.js         | 2 ++
 src/components/views/messages/MAudioBody.js                 | 2 ++
 src/components/views/messages/MFileBody.js                  | 2 ++
 src/components/views/messages/MImageBody.js                 | 2 ++
 src/components/views/messages/MJitsiWidgetEvent.tsx         | 2 ++
 src/components/views/messages/MKeyVerificationConclusion.js | 2 ++
 src/components/views/messages/MKeyVerificationRequest.js    | 2 ++
 src/components/views/messages/MStickerBody.js               | 2 ++
 src/components/views/messages/MVideoBody.tsx                | 2 ++
 src/components/views/messages/MessageActionBar.js           | 2 ++
 src/components/views/messages/MessageEvent.js               | 2 ++
 src/components/views/messages/MessageTimestamp.js           | 2 ++
 src/components/views/messages/MjolnirBody.js                | 2 ++
 src/components/views/messages/ReactionsRow.js               | 2 ++
 src/components/views/messages/ReactionsRowButton.js         | 2 ++
 src/components/views/messages/ReactionsRowButtonTooltip.js  | 2 ++
 src/components/views/messages/RoomAvatarEvent.js            | 2 ++
 src/components/views/messages/RoomCreate.js                 | 2 ++
 src/components/views/messages/SenderProfile.js              | 2 ++
 src/components/views/messages/TextualBody.js                | 2 ++
 src/components/views/messages/TextualEvent.js               | 2 ++
 src/components/views/messages/TileErrorBoundary.js          | 2 ++
 src/components/views/messages/ViewSourceEvent.js            | 2 ++
 41 files changed, 82 insertions(+)

diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx
index c4feaac8ae..4c7852def3 100644
--- a/src/components/views/emojipicker/Category.tsx
+++ b/src/components/views/emojipicker/Category.tsx
@@ -21,6 +21,7 @@ import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPic
 import LazyRenderList from "../elements/LazyRenderList";
 import {DATA_BY_CATEGORY, IEmoji} from "../../../emoji";
 import Emoji from './Emoji';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const OVERFLOW_ROWS = 3;
 
@@ -47,6 +48,7 @@ interface IProps {
     onMouseLeave(emoji: IEmoji): void;
 }
 
+@replaceableComponent("views.emojipicker.Category")
 class Category extends React.PureComponent<IProps> {
     private renderEmojiRow = (rowIndex: number) => {
         const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx
index 5d715fb935..5d7665ce98 100644
--- a/src/components/views/emojipicker/Emoji.tsx
+++ b/src/components/views/emojipicker/Emoji.tsx
@@ -19,6 +19,7 @@ import React from 'react';
 
 import {MenuItem} from "../../structures/ContextMenu";
 import {IEmoji} from "../../../emoji";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     emoji: IEmoji;
@@ -28,6 +29,7 @@ interface IProps {
     onMouseLeave(emoji: IEmoji): void;
 }
 
+@replaceableComponent("views.emojipicker.Emoji")
 class Emoji extends React.PureComponent<IProps> {
     render() {
         const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx
index bf0481c51c..6d7b90c8a6 100644
--- a/src/components/views/emojipicker/EmojiPicker.tsx
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -26,6 +26,7 @@ import Search from "./Search";
 import Preview from "./Preview";
 import QuickReactions from "./QuickReactions";
 import Category, {ICategory, CategoryKey} from "./Category";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export const CATEGORY_HEADER_HEIGHT = 22;
 export const EMOJI_HEIGHT = 37;
@@ -47,6 +48,7 @@ interface IState {
     viewportHeight: number;
 }
 
+@replaceableComponent("views.emojipicker.EmojiPicker")
 class EmojiPicker extends React.Component<IProps, IState> {
     private readonly recentlyUsed: IEmoji[];
     private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
diff --git a/src/components/views/emojipicker/Header.tsx b/src/components/views/emojipicker/Header.tsx
index 9a93722483..693f86ad73 100644
--- a/src/components/views/emojipicker/Header.tsx
+++ b/src/components/views/emojipicker/Header.tsx
@@ -21,12 +21,14 @@ import classNames from "classnames";
 import {_t} from "../../../languageHandler";
 import {Key} from "../../../Keyboard";
 import {CategoryKey, ICategory} from "./Category";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     categories: ICategory[];
     onAnchorClick(id: CategoryKey): void
 }
 
+@replaceableComponent("views.emojipicker.Header")
 class Header extends React.PureComponent<IProps> {
     private findNearestEnabled(index: number, delta: number) {
         index += this.props.categories.length;
diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx
index 69bfdf4d1c..e0952ec73e 100644
--- a/src/components/views/emojipicker/Preview.tsx
+++ b/src/components/views/emojipicker/Preview.tsx
@@ -18,11 +18,13 @@ limitations under the License.
 import React from 'react';
 
 import {IEmoji} from "../../../emoji";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     emoji: IEmoji;
 }
 
+@replaceableComponent("views.emojipicker.Preview")
 class Preview extends React.PureComponent<IProps> {
     render() {
         const {
diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx
index 0477ecfb93..a250aca458 100644
--- a/src/components/views/emojipicker/QuickReactions.tsx
+++ b/src/components/views/emojipicker/QuickReactions.tsx
@@ -20,6 +20,7 @@ import React from 'react';
 import { _t } from '../../../languageHandler';
 import {getEmojiFromUnicode, IEmoji} from "../../../emoji";
 import Emoji from "./Emoji";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // We use the variation-selector Heart in Quick Reactions for some reason
 const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
@@ -39,6 +40,7 @@ interface IState {
     hover?: IEmoji;
 }
 
+@replaceableComponent("views.emojipicker.QuickReactions")
 class QuickReactions extends React.Component<IProps, IState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx
index dbef0eadbe..e86d183aba 100644
--- a/src/components/views/emojipicker/ReactionPicker.tsx
+++ b/src/components/views/emojipicker/ReactionPicker.tsx
@@ -21,6 +21,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event";
 import EmojiPicker from "./EmojiPicker";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import dis from "../../../dispatcher/dispatcher";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     mxEvent: MatrixEvent;
@@ -32,6 +33,7 @@ interface IState {
     selectedEmojis: Set<string>;
 }
 
+@replaceableComponent("views.emojipicker.ReactionPicker")
 class ReactionPicker extends React.Component<IProps, IState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx
index fe1fecec7b..abe3e026be 100644
--- a/src/components/views/emojipicker/Search.tsx
+++ b/src/components/views/emojipicker/Search.tsx
@@ -19,6 +19,7 @@ import React from 'react';
 
 import { _t } from '../../../languageHandler';
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     query: string;
@@ -26,6 +27,7 @@ interface IProps {
     onEnter(): void;
 }
 
+@replaceableComponent("views.emojipicker.Search")
 class Search extends React.PureComponent<IProps> {
     private inputRef = React.createRef<HTMLInputElement>();
 
diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js
index 0c09b6ed05..dc48c01acb 100644
--- a/src/components/views/groups/GroupInviteTile.js
+++ b/src/components/views/groups/GroupInviteTile.js
@@ -26,8 +26,10 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // XXX this class copies a lot from RoomTile.js
+@replaceableComponent("views.groups.GroupInviteTile")
 export default class GroupInviteTile extends React.Component {
     static propTypes: {
         group: PropTypes.object.isRequired,
diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js
index 600a466601..d5b3f9aec7 100644
--- a/src/components/views/groups/GroupMemberList.js
+++ b/src/components/views/groups/GroupMemberList.js
@@ -26,9 +26,11 @@ import AccessibleButton from '../elements/AccessibleButton';
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const INITIAL_LOAD_NUM_MEMBERS = 30;
 
+@replaceableComponent("views.groups.GroupMemberList")
 export default class GroupMemberList extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js
index 13617cf681..e8285803b0 100644
--- a/src/components/views/groups/GroupMemberTile.js
+++ b/src/components/views/groups/GroupMemberTile.js
@@ -22,7 +22,9 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import { GroupMemberType } from '../../../groups';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupMemberTile")
 export default class GroupMemberTile extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js
index d42059551e..5399125d9f 100644
--- a/src/components/views/groups/GroupPublicityToggle.js
+++ b/src/components/views/groups/GroupPublicityToggle.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import GroupStore from '../../../stores/GroupStore';
 import ToggleSwitch from "../elements/ToggleSwitch";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupPublicityTile")
 export default class GroupPublicityToggle extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js
index 50bbd26029..227a17e995 100644
--- a/src/components/views/groups/GroupRoomInfo.js
+++ b/src/components/views/groups/GroupRoomInfo.js
@@ -24,7 +24,9 @@ import { _t } from '../../../languageHandler';
 import GroupStore from '../../../stores/GroupStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupRoomInfo")
 export default class GroupRoomInfo extends React.Component {
     static contextType = MatrixClientContext;
 
diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js
index 9bb46db47c..f8a90f9222 100644
--- a/src/components/views/groups/GroupRoomList.js
+++ b/src/components/views/groups/GroupRoomList.js
@@ -21,9 +21,11 @@ import PropTypes from 'prop-types';
 import { showGroupAddRoomDialog } from '../../../GroupAddressPicker';
 import AccessibleButton from '../elements/AccessibleButton';
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const INITIAL_LOAD_NUM_ROOMS = 30;
 
+@replaceableComponent("views.groups.GroupRoomList")
 export default class GroupRoomList extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js
index 85aa56d055..8b25437f71 100644
--- a/src/components/views/groups/GroupRoomTile.js
+++ b/src/components/views/groups/GroupRoomTile.js
@@ -20,7 +20,9 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import { GroupRoomType } from '../../../groups';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupRoomTile")
 class GroupRoomTile extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js
index dcc749b01f..bb1714c9f2 100644
--- a/src/components/views/groups/GroupTile.js
+++ b/src/components/views/groups/GroupTile.js
@@ -21,9 +21,11 @@ import * as sdk from '../../../index';
 import dis from '../../../dispatcher/dispatcher';
 import FlairStore from '../../../stores/FlairStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function nop() {}
 
+@replaceableComponent("views.groups.GroupTile")
 class GroupTile extends React.Component {
     static propTypes = {
         groupId: PropTypes.string.isRequired,
diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js
index 9209106c8f..5b537d7377 100644
--- a/src/components/views/groups/GroupUserSettings.js
+++ b/src/components/views/groups/GroupUserSettings.js
@@ -18,7 +18,9 @@ import React from 'react';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.groups.GroupUserSettings")
 export default class GroupUserSettings extends React.Component {
     static contextType = MatrixClientContext;
 
diff --git a/src/components/views/messages/DateSeparator.js b/src/components/views/messages/DateSeparator.js
index ef4b5d16d1..82ce8dc4ae 100644
--- a/src/components/views/messages/DateSeparator.js
+++ b/src/components/views/messages/DateSeparator.js
@@ -19,6 +19,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import {formatFullDateNoTime} from '../../../DateUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function getdaysArray() {
     return [
@@ -32,6 +33,7 @@ function getdaysArray() {
     ];
 }
 
+@replaceableComponent("views.messages.DateSeparator")
 export default class DateSeparator extends React.Component {
     static propTypes = {
         ts: PropTypes.number.isRequired,
diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
index 0967be937a..7f100fbb92 100644
--- a/src/components/views/messages/EditHistoryMessage.js
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -27,12 +27,14 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import Modal from '../../../Modal';
 import classNames from 'classnames';
 import RedactedBody from "./RedactedBody";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function getReplacedContent(event) {
     const originalContent = event.getOriginalContent();
     return originalContent["m.new_content"] || originalContent;
 }
 
+@replaceableComponent("views.messages.EditHistoryMessage")
 export default class EditHistoryMessage extends React.PureComponent {
     static propTypes = {
         // the message event being edited
diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js
index 587dee4513..59a10e17f3 100644
--- a/src/components/views/messages/MAudioBody.js
+++ b/src/components/views/messages/MAudioBody.js
@@ -23,7 +23,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { decryptFile } from '../../../utils/DecryptFile';
 import { _t } from '../../../languageHandler';
 import InlineSpinner from '../elements/InlineSpinner';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MAudioBody")
 export default class MAudioBody extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index 676f0b7986..e9893f99b6 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -26,6 +26,7 @@ import Tinter from '../../../Tinter';
 import request from 'browser-request';
 import Modal from '../../../Modal';
 import AccessibleButton from "../elements/AccessibleButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 
 // A cached tinted copy of require("../../../../res/img/download.svg")
@@ -116,6 +117,7 @@ function computedStyle(element) {
     return cssText;
 }
 
+@replaceableComponent("views.messages.MFileBody")
 export default class MFileBody extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 771d12accd..59c5b4e66b 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -27,7 +27,9 @@ import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import InlineSpinner from '../elements/InlineSpinner';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MImageBody")
 export default class MImageBody extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx
index 6031ede8fa..626efe1f36 100644
--- a/src/components/views/messages/MJitsiWidgetEvent.tsx
+++ b/src/components/views/messages/MJitsiWidgetEvent.tsx
@@ -21,11 +21,13 @@ import WidgetStore from "../../../stores/WidgetStore";
 import EventTileBubble from "./EventTileBubble";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     mxEvent: MatrixEvent;
 }
 
+@replaceableComponent("views.messages.MJitsiWidgetEvent")
 export default class MJitsiWidgetEvent extends React.PureComponent<IProps> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js
index 880299d29d..75d20131c0 100644
--- a/src/components/views/messages/MKeyVerificationConclusion.js
+++ b/src/components/views/messages/MKeyVerificationConclusion.js
@@ -22,7 +22,9 @@ import { _t } from '../../../languageHandler';
 import {getNameForEventRoom, userLabelForEventRoom}
     from '../../../utils/KeyVerificationStateObserver';
 import EventTileBubble from "./EventTileBubble";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MKeyVerificationConclusion")
 export default class MKeyVerificationConclusion extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js
index d9594091c5..988606a766 100644
--- a/src/components/views/messages/MKeyVerificationRequest.js
+++ b/src/components/views/messages/MKeyVerificationRequest.js
@@ -25,7 +25,9 @@ import dis from "../../../dispatcher/dispatcher";
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import {Action} from "../../../dispatcher/actions";
 import EventTileBubble from "./EventTileBubble";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MKeyVerificationRequest")
 export default class MKeyVerificationRequest extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/MStickerBody.js b/src/components/views/messages/MStickerBody.js
index 9839080661..54eb7649b4 100644
--- a/src/components/views/messages/MStickerBody.js
+++ b/src/components/views/messages/MStickerBody.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import MImageBody from './MImageBody';
 import * as sdk from '../../../index';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MStickerBody")
 export default class MStickerBody extends MImageBody {
     // Mostly empty to prevent default behaviour of MImageBody
     onClick(ev) {
diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx
index ce4a4eda76..89985dee7d 100644
--- a/src/components/views/messages/MVideoBody.tsx
+++ b/src/components/views/messages/MVideoBody.tsx
@@ -22,6 +22,7 @@ import { decryptFile } from '../../../utils/DecryptFile';
 import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import InlineSpinner from '../elements/InlineSpinner';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     /* the MatrixEvent to show */
@@ -38,6 +39,7 @@ interface IState {
     fetchingData: boolean,
 }
 
+@replaceableComponent("views.messages.MVideoBody")
 export default class MVideoBody extends React.PureComponent<IProps, IState> {
     private videoRef = React.createRef<HTMLVideoElement>();
 
diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js
index c94f296eac..c33debe3f5 100644
--- a/src/components/views/messages/MessageActionBar.js
+++ b/src/components/views/messages/MessageActionBar.js
@@ -28,6 +28,7 @@ import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
 import RoomContext from "../../../contexts/RoomContext";
 import Toolbar from "../../../accessibility/Toolbar";
 import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
     const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@@ -101,6 +102,7 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => {
     </React.Fragment>;
 };
 
+@replaceableComponent("views.messages.MessageActionBar")
 export default class MessageActionBar extends React.PureComponent {
     static propTypes = {
         mxEvent: PropTypes.object.isRequired,
diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js
index f93813fe79..866e0f521d 100644
--- a/src/components/views/messages/MessageEvent.js
+++ b/src/components/views/messages/MessageEvent.js
@@ -21,7 +21,9 @@ import SettingsStore from "../../../settings/SettingsStore";
 import {Mjolnir} from "../../../mjolnir/Mjolnir";
 import RedactedBody from "./RedactedBody";
 import UnknownBody from "./UnknownBody";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MessageEvent")
 export default class MessageEvent extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/MessageTimestamp.js b/src/components/views/messages/MessageTimestamp.js
index 199a6f47ce..c9bdb8937e 100644
--- a/src/components/views/messages/MessageTimestamp.js
+++ b/src/components/views/messages/MessageTimestamp.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import {formatFullDate, formatTime} from '../../../DateUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MessageTimestamp")
 export default class MessageTimestamp extends React.Component {
     static propTypes = {
         ts: PropTypes.number.isRequired,
diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js
index baaee91657..4368fd936c 100644
--- a/src/components/views/messages/MjolnirBody.js
+++ b/src/components/views/messages/MjolnirBody.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import {_t} from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.MjolnirBody")
 export default class MjolnirBody extends React.Component {
     static propTypes = {
         mxEvent: PropTypes.object.isRequired,
diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js
index 3451cdbb2d..d5c8ea2ac9 100644
--- a/src/components/views/messages/ReactionsRow.js
+++ b/src/components/views/messages/ReactionsRow.js
@@ -21,10 +21,12 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import { isContentActionable } from '../../../utils/EventUtils';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // The maximum number of reactions to initially show on a message.
 const MAX_ITEMS_WHEN_LIMITED = 8;
 
+@replaceableComponent("views.messages.ReactionsRow")
 export default class ReactionsRow extends React.PureComponent {
     static propTypes = {
         // The event we're displaying reactions for
diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js
index bb8d9a3b6e..06421c02a2 100644
--- a/src/components/views/messages/ReactionsRowButton.js
+++ b/src/components/views/messages/ReactionsRowButton.js
@@ -23,7 +23,9 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
 import dis from "../../../dispatcher/dispatcher";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.ReactionsRowButton")
 export default class ReactionsRowButton extends React.PureComponent {
     static propTypes = {
         // The event we're displaying reactions for
diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js
index 2b90175722..5ecdfe311d 100644
--- a/src/components/views/messages/ReactionsRowButtonTooltip.js
+++ b/src/components/views/messages/ReactionsRowButtonTooltip.js
@@ -22,7 +22,9 @@ import * as sdk from '../../../index';
 import { unicodeToShortcode } from '../../../HtmlUtils';
 import { _t } from '../../../languageHandler';
 import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.ReactionsRowButtonTooltip")
 export default class ReactionsRowButtonTooltip extends React.PureComponent {
     static propTypes = {
         // The event we're displaying reactions for
diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js
index f526d080cc..ba860216f0 100644
--- a/src/components/views/messages/RoomAvatarEvent.js
+++ b/src/components/views/messages/RoomAvatarEvent.js
@@ -23,7 +23,9 @@ import { _t } from '../../../languageHandler';
 import * as sdk from '../../../index';
 import Modal from '../../../Modal';
 import AccessibleButton from '../elements/AccessibleButton';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.RoomAvatarEvent")
 export default class RoomAvatarEvent extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js
index 479592aa42..3e02884c02 100644
--- a/src/components/views/messages/RoomCreate.js
+++ b/src/components/views/messages/RoomCreate.js
@@ -23,7 +23,9 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
 import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import EventTileBubble from "./EventTileBubble";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.RoomCreate")
 export default class RoomCreate extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js
index d2db05252c..bd10526799 100644
--- a/src/components/views/messages/SenderProfile.js
+++ b/src/components/views/messages/SenderProfile.js
@@ -20,7 +20,9 @@ import Flair from '../elements/Flair.js';
 import FlairStore from '../../../stores/FlairStore';
 import {getUserNameColorClass} from '../../../utils/FormattingUtils';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.SenderProfile")
 export default class SenderProfile extends React.Component {
     static propTypes = {
         mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 04db7bd725..b0eb6f2f35 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -35,7 +35,9 @@ import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
 import {toRightOf} from "../../structures/ContextMenu";
 import {copyPlaintext} from "../../../utils/strings";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.TextualBody")
 export default class TextualBody extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.js
index 99e94147f7..a020cc6c52 100644
--- a/src/components/views/messages/TextualEvent.js
+++ b/src/components/views/messages/TextualEvent.js
@@ -18,7 +18,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as TextForEvent from "../../../TextForEvent";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.TextualEvent")
 export default class TextualEvent extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/messages/TileErrorBoundary.js b/src/components/views/messages/TileErrorBoundary.js
index 9b67e32548..0e9a7b6128 100644
--- a/src/components/views/messages/TileErrorBoundary.js
+++ b/src/components/views/messages/TileErrorBoundary.js
@@ -20,7 +20,9 @@ import { _t } from '../../../languageHandler';
 import * as sdk from '../../../index';
 import Modal from '../../../Modal';
 import SdkConfig from "../../../SdkConfig";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.TileErrorBoundary")
 export default class TileErrorBoundary extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js
index 9064fc3b68..adc7a248cd 100644
--- a/src/components/views/messages/ViewSourceEvent.js
+++ b/src/components/views/messages/ViewSourceEvent.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.messages.ViewSourceEvent")
 export default class ViewSourceEvent extends React.PureComponent {
     static propTypes = {
         /* the MatrixEvent to show */

From c5935dbc6169f44c32becb8163aff99c42bbae0f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 20:12:00 -0700
Subject: [PATCH 342/389] Batch of views getting replaceableComponent
 decorators

---
 src/components/views/right_panel/GroupHeaderButtons.tsx    | 2 ++
 src/components/views/right_panel/HeaderButton.tsx          | 2 ++
 src/components/views/right_panel/HeaderButtons.tsx         | 2 ++
 src/components/views/right_panel/RoomHeaderButtons.tsx     | 2 ++
 src/components/views/right_panel/VerificationPanel.tsx     | 2 ++
 src/components/views/room_settings/AliasSettings.js        | 2 ++
 src/components/views/room_settings/RelatedGroupSettings.js | 2 ++
 src/components/views/room_settings/RoomProfileSettings.js  | 2 ++
 src/components/views/room_settings/UrlPreviewSettings.js   | 3 ++-
 src/components/views/rooms/AppsDrawer.js                   | 2 ++
 src/components/views/rooms/Autocomplete.tsx                | 2 ++
 src/components/views/rooms/AuxPanel.tsx                    | 2 ++
 src/components/views/rooms/BasicMessageComposer.tsx        | 2 ++
 src/components/views/rooms/EditMessageComposer.js          | 2 ++
 src/components/views/rooms/EntityTile.js                   | 2 ++
 src/components/views/rooms/EventTile.js                    | 2 ++
 src/components/views/rooms/ForwardMessage.js               | 3 ++-
 src/components/views/rooms/LinkPreviewWidget.js            | 2 ++
 src/components/views/rooms/MemberList.js                   | 2 ++
 src/components/views/rooms/MemberTile.js                   | 2 ++
 src/components/views/rooms/MessageComposer.js              | 2 ++
 src/components/views/rooms/MessageComposerFormatBar.js     | 2 ++
 src/components/views/rooms/NotificationBadge.tsx           | 2 ++
 src/components/views/rooms/PinnedEventTile.js              | 2 ++
 src/components/views/rooms/PinnedEventsPanel.js            | 2 ++
 src/components/views/rooms/PresenceLabel.js                | 3 ++-
 src/components/views/rooms/ReadReceiptMarker.js            | 2 ++
 src/components/views/rooms/ReplyPreview.js                 | 2 ++
 src/components/views/rooms/RoomBreadcrumbs.tsx             | 2 ++
 src/components/views/rooms/RoomDetailList.js               | 2 ++
 src/components/views/rooms/RoomDetailRow.js                | 2 ++
 src/components/views/rooms/RoomHeader.js                   | 2 ++
 src/components/views/rooms/RoomList.tsx                    | 2 ++
 src/components/views/rooms/RoomPreviewBar.js               | 2 ++
 src/components/views/rooms/RoomSublist.tsx                 | 2 ++
 src/components/views/rooms/RoomTile.tsx                    | 2 ++
 src/components/views/rooms/RoomUpgradeWarningBar.js        | 2 ++
 src/components/views/rooms/SearchBar.js                    | 2 ++
 src/components/views/rooms/SearchResultTile.js             | 2 ++
 src/components/views/rooms/SendMessageComposer.js          | 2 ++
 src/components/views/rooms/SimpleRoomHeader.js             | 2 ++
 src/components/views/rooms/Stickerpicker.js                | 2 ++
 src/components/views/rooms/TemporaryTile.tsx               | 2 ++
 src/components/views/rooms/ThirdPartyMemberInfo.js         | 2 ++
 src/components/views/rooms/TopUnreadMessagesBar.js         | 2 ++
 src/components/views/rooms/WhoIsTypingTile.js              | 2 ++
 46 files changed, 92 insertions(+), 3 deletions(-)

diff --git a/src/components/views/right_panel/GroupHeaderButtons.tsx b/src/components/views/right_panel/GroupHeaderButtons.tsx
index dd4a82e645..f006975b08 100644
--- a/src/components/views/right_panel/GroupHeaderButtons.tsx
+++ b/src/components/views/right_panel/GroupHeaderButtons.tsx
@@ -26,6 +26,7 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import {Action} from "../../../dispatcher/actions";
 import {ActionPayload} from "../../../dispatcher/payloads";
 import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const GROUP_PHASES = [
     RightPanelPhases.GroupMemberInfo,
@@ -38,6 +39,7 @@ const ROOM_PHASES = [
 
 interface IProps {}
 
+@replaceableComponent("views.right_panel.GroupHeaderButtons")
 export default class GroupHeaderButtons extends HeaderButtons {
     constructor(props: IProps) {
         super(props, HeaderKind.Group);
diff --git a/src/components/views/right_panel/HeaderButton.tsx b/src/components/views/right_panel/HeaderButton.tsx
index 7f682e2d89..2bc360e380 100644
--- a/src/components/views/right_panel/HeaderButton.tsx
+++ b/src/components/views/right_panel/HeaderButton.tsx
@@ -22,6 +22,7 @@ import React from 'react';
 import classNames from 'classnames';
 import Analytics from '../../../Analytics';
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // Whether this button is highlighted
@@ -41,6 +42,7 @@ interface IProps {
 
 // TODO: replace this, the composer buttons and the right panel buttons with a unified
 // representation
+@replaceableComponent("views.right_panel.HeaderButton")
 export default class HeaderButton extends React.Component<IProps> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx
index 543c7c067f..2144292679 100644
--- a/src/components/views/right_panel/HeaderButtons.tsx
+++ b/src/components/views/right_panel/HeaderButtons.tsx
@@ -28,6 +28,7 @@ import {
     SetRightPanelPhaseRefireParams,
 } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
 import {EventSubscription} from "fbemitter";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export enum HeaderKind {
   Room = "room",
@@ -41,6 +42,7 @@ interface IState {
 
 interface IProps {}
 
+@replaceableComponent("views.right_panel.HeaderButtons")
 export default abstract class HeaderButtons extends React.Component<IProps, IState> {
     private storeToken: EventSubscription;
     private dispatcherRef: string;
diff --git a/src/components/views/right_panel/RoomHeaderButtons.tsx b/src/components/views/right_panel/RoomHeaderButtons.tsx
index c2364546fd..0571622e64 100644
--- a/src/components/views/right_panel/RoomHeaderButtons.tsx
+++ b/src/components/views/right_panel/RoomHeaderButtons.tsx
@@ -26,6 +26,7 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import {Action} from "../../../dispatcher/actions";
 import {ActionPayload} from "../../../dispatcher/payloads";
 import RightPanelStore from "../../../stores/RightPanelStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const ROOM_INFO_PHASES = [
     RightPanelPhases.RoomSummary,
@@ -37,6 +38,7 @@ const ROOM_INFO_PHASES = [
     RightPanelPhases.Room3pidMemberInfo,
 ];
 
+@replaceableComponent("views.right_panel.RoomHeaderButtons")
 export default class RoomHeaderButtons extends HeaderButtons {
     constructor(props) {
         super(props, HeaderKind.Room);
diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx
index f584a63209..ac01c953b9 100644
--- a/src/components/views/right_panel/VerificationPanel.tsx
+++ b/src/components/views/right_panel/VerificationPanel.tsx
@@ -36,6 +36,7 @@ import {
     PHASE_CANCELLED,
 } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 import Spinner from "../elements/Spinner";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // XXX: Should be defined in matrix-js-sdk
 enum VerificationPhase {
@@ -65,6 +66,7 @@ interface IState {
     reciprocateQREvent?: ReciprocateQRCode;
 }
 
+@replaceableComponent("views.right_panel.VerificationPanel")
 export default class VerificationPanel extends React.PureComponent<IProps, IState> {
     private hasVerifier: boolean;
 
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js
index eb9276b729..ee8232ebd7 100644
--- a/src/components/views/room_settings/AliasSettings.js
+++ b/src/components/views/room_settings/AliasSettings.js
@@ -26,6 +26,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
 import AccessibleButton from "../elements/AccessibleButton";
 import Modal from "../../../Modal";
 import RoomPublishSetting from "./RoomPublishSetting";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 class EditableAliasesList extends EditableItemList {
     constructor(props) {
@@ -74,6 +75,7 @@ class EditableAliasesList extends EditableItemList {
     }
 }
 
+@replaceableComponent("views.room_settings.AliasSettings")
 export default class AliasSettings extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js
index af3f58f9db..f82e238722 100644
--- a/src/components/views/room_settings/RelatedGroupSettings.js
+++ b/src/components/views/room_settings/RelatedGroupSettings.js
@@ -22,9 +22,11 @@ import { _t } from '../../../languageHandler';
 import Modal from '../../../Modal';
 import ErrorDialog from "../dialogs/ErrorDialog";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const GROUP_ID_REGEX = /\+\S+:\S+/;
 
+@replaceableComponent("views.room_settings.RelatedGroupSettings")
 export default class RelatedGroupSettings extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js
index 65acc802dc..563368384b 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.js
@@ -20,8 +20,10 @@ import {_t} from "../../../languageHandler";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import Field from "../elements/Field";
 import * as sdk from "../../../index";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // TODO: Merge with ProfileSettings?
+@replaceableComponent("views.room_settings.RoomProfileSettings")
 export default class RoomProfileSettings extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js
index 114e9b2894..7b04e296e5 100644
--- a/src/components/views/room_settings/UrlPreviewSettings.js
+++ b/src/components/views/room_settings/UrlPreviewSettings.js
@@ -26,8 +26,9 @@ import dis from "../../../dispatcher/dispatcher";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {Action} from "../../../dispatcher/actions";
 import {SettingLevel} from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.room_settings.UrlPreviewSettings")
 export default class UrlPreviewSettings extends React.Component {
     static propTypes = {
         room: PropTypes.object,
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index aa7120bbe6..3ef8d71682 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -35,7 +35,9 @@ import PercentageDistributor from "../../../resizer/distributors/percentage";
 import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayoutStore";
 import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
 import {useStateCallback} from "../../../hooks/useStateCallback";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.AppsDrawer")
 export default class AppsDrawer extends React.Component {
     static propTypes = {
         userId: PropTypes.string.isRequired,
diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx
index 15af75084a..a4dcba11a3 100644
--- a/src/components/views/rooms/Autocomplete.tsx
+++ b/src/components/views/rooms/Autocomplete.tsx
@@ -23,6 +23,7 @@ import {Room} from 'matrix-js-sdk/src/models/room';
 
 import SettingsStore from "../../../settings/SettingsStore";
 import Autocompleter from '../../../autocomplete/Autocompleter';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const COMPOSER_SELECTED = 0;
 
@@ -49,6 +50,7 @@ interface IState {
     forceComplete: boolean;
 }
 
+@replaceableComponent("views.rooms.Autocomplete")
 export default class Autocomplete extends React.PureComponent<IProps, IState> {
     autocompleter: Autocompleter;
     queryRequested: string;
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index d193b98ec1..3d431f7c67 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -27,6 +27,7 @@ import {UIFeature} from "../../../settings/UIFeature";
 import { ResizeNotifier } from "../../../utils/ResizeNotifier";
 import CallViewForRoom from '../voip/CallViewForRoom';
 import {objectHasDiff} from "../../../utils/objects";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // js-sdk room object
@@ -58,6 +59,7 @@ interface IState {
     counters: Counter[],
 }
 
+@replaceableComponent("views.rooms.AuxPanel")
 export default class AuxPanel extends React.Component<IProps, IState> {
     static defaultProps = {
         showApps: true,
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index 017ce77166..829809ad49 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -46,6 +46,7 @@ import {IDiff} from "../../../editor/diff";
 import AutocompleteWrapperModel from "../../../editor/autocomplete";
 import DocumentPosition from "../../../editor/position";
 import {ICompletion} from "../../../autocomplete/Autocompleter";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // matches emoticons which follow the start of a line or whitespace
 const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
@@ -105,6 +106,7 @@ interface IState {
     completionIndex?: number;
 }
 
+@replaceableComponent("views.rooms.BasixMessageEditor")
 export default class BasicMessageEditor extends React.Component<IProps, IState> {
     private editorRef = createRef<HTMLDivElement>();
     private autocompleteRef = createRef<Autocomplete>();
diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js
index c59b3555b9..6ecb2bd549 100644
--- a/src/components/views/rooms/EditMessageComposer.js
+++ b/src/components/views/rooms/EditMessageComposer.js
@@ -34,6 +34,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {Action} from "../../../dispatcher/actions";
 import SettingsStore from "../../../settings/SettingsStore";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function _isReply(mxEvent) {
     const relatesTo = mxEvent.getContent()["m.relates_to"];
@@ -102,6 +103,7 @@ function createEditContent(model, editedEvent) {
     }, contentBody);
 }
 
+@replaceableComponent("views.rooms.EditMessageComposer")
 export default class EditMessageComposer extends React.Component {
     static propTypes = {
         // the message event being edited
diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js
index 9017e4aa3e..75b03739b9 100644
--- a/src/components/views/rooms/EntityTile.js
+++ b/src/components/views/rooms/EntityTile.js
@@ -23,6 +23,7 @@ import AccessibleButton from '../elements/AccessibleButton';
 import { _t } from '../../../languageHandler';
 import classNames from "classnames";
 import E2EIcon from './E2EIcon';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const PRESENCE_CLASS = {
     "offline": "mx_EntityTile_offline",
@@ -50,6 +51,7 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
     }
 }
 
+@replaceableComponent("views.rooms.EntityTile")
 class EntityTile extends React.Component {
     static propTypes = {
         name: PropTypes.string,
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index a705e92d9c..1366d9b603 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -39,6 +39,7 @@ import {WidgetType} from "../../../widgets/WidgetType";
 import RoomAvatar from "../avatars/RoomAvatar";
 import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
 import {objectHasDiff} from "../../../utils/objects";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const eventTileTypes = {
     'm.room.message': 'messages.MessageEvent',
@@ -146,6 +147,7 @@ const MAX_READ_AVATARS = 5;
 // |    '--------------------------------------'              |
 // '----------------------------------------------------------'
 
+@replaceableComponent("views.rooms.EventTile")
 export default class EventTile extends React.Component {
     static propTypes = {
         /* the MatrixEvent to show */
diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js
index b85dd2c8df..222895ef04 100644
--- a/src/components/views/rooms/ForwardMessage.js
+++ b/src/components/views/rooms/ForwardMessage.js
@@ -19,8 +19,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import {Key} from '../../../Keyboard';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.rooms.FowardMessage")
 export default class ForwardMessage extends React.Component {
     static propTypes = {
         onCancelClick: PropTypes.func.isRequired,
diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js
index 2a053bf467..39c9f0bcf7 100644
--- a/src/components/views/rooms/LinkPreviewWidget.js
+++ b/src/components/views/rooms/LinkPreviewWidget.js
@@ -25,7 +25,9 @@ import * as sdk from "../../../index";
 import Modal from "../../../Modal";
 import * as ImageUtils from "../../../ImageUtils";
 import { _t } from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.LinkPreviewWidget")
 export default class LinkPreviewWidget extends React.Component {
     static propTypes = {
         link: PropTypes.string.isRequired, // the URL being previewed
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index d4d618c821..593132a283 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -29,6 +29,7 @@ import BaseCard from "../right_panel/BaseCard";
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import RoomAvatar from "../avatars/RoomAvatar";
 import RoomName from "../elements/RoomName";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const INITIAL_LOAD_NUM_MEMBERS = 30;
 const INITIAL_LOAD_NUM_INVITED = 5;
@@ -38,6 +39,7 @@ const SHOW_MORE_INCREMENT = 100;
 // matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
 const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
 
+@replaceableComponent("views.rooms.MemberList")
 export default class MemberList extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js
index a43b42b6d3..f8df7ed78f 100644
--- a/src/components/views/rooms/MemberTile.js
+++ b/src/components/views/rooms/MemberTile.js
@@ -23,7 +23,9 @@ import dis from "../../../dispatcher/dispatcher";
 import { _t } from '../../../languageHandler';
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.MemberTile")
 export default class MemberTile extends React.Component {
     static propTypes = {
         member: PropTypes.any.isRequired, // RoomMember
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index c03178cdf7..ccf097c4fd 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -32,6 +32,7 @@ import {UIFeature} from "../../../settings/UIFeature";
 import WidgetStore from "../../../stores/WidgetStore";
 import {UPDATE_EVENT} from "../../../stores/AsyncStore";
 import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function ComposerAvatar(props) {
     const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@@ -168,6 +169,7 @@ class UploadButton extends React.Component {
     }
 }
 
+@replaceableComponent("views.rooms.MessageComposer")
 export default class MessageComposer extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.js
index 71aef1e833..d2539b1ef4 100644
--- a/src/components/views/rooms/MessageComposerFormatBar.js
+++ b/src/components/views/rooms/MessageComposerFormatBar.js
@@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import classNames from 'classnames';
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.MessageComposerFormatBar")
 export default class MessageComposerFormatBar extends React.PureComponent {
     static propTypes = {
         onAction: PropTypes.func.isRequired,
diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx
index 8b996d3238..36a52e260d 100644
--- a/src/components/views/rooms/NotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge.tsx
@@ -21,6 +21,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import AccessibleButton from "../elements/AccessibleButton";
 import { XOR } from "../../../@types/common";
 import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     notification: NotificationState;
@@ -48,6 +49,7 @@ interface IState {
     showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
 }
 
+@replaceableComponent("views.rooms.NotificationBadge")
 export default class NotificationBadge extends React.PureComponent<XOR<IProps, IClickableProps>, IState> {
     private countWatcherRef: string;
 
diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js
index 9fad0c2391..2259cad7fb 100644
--- a/src/components/views/rooms/PinnedEventTile.js
+++ b/src/components/views/rooms/PinnedEventTile.js
@@ -23,7 +23,9 @@ import MessageEvent from "../messages/MessageEvent";
 import MemberAvatar from "../avatars/MemberAvatar";
 import { _t } from '../../../languageHandler';
 import {formatFullDate} from '../../../DateUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.PinnedEventTile")
 export default class PinnedEventTile extends React.Component {
     static propTypes = {
         mxRoom: PropTypes.object.isRequired,
diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js
index 3ea0299976..285829bf63 100644
--- a/src/components/views/rooms/PinnedEventsPanel.js
+++ b/src/components/views/rooms/PinnedEventsPanel.js
@@ -22,7 +22,9 @@ import AccessibleButton from "../elements/AccessibleButton";
 import PinnedEventTile from "./PinnedEventTile";
 import { _t } from '../../../languageHandler';
 import PinningUtils from "../../../utils/PinningUtils";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.PinnedEventsPanel")
 export default class PinnedEventsPanel extends React.Component {
     static propTypes = {
         // The Room from the js-sdk we're going to show pinned events for
diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js
index ff1460ca21..ca21afe63d 100644
--- a/src/components/views/rooms/PresenceLabel.js
+++ b/src/components/views/rooms/PresenceLabel.js
@@ -18,8 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-
+@replaceableComponent("views.rooms.PresenceLabel")
 export default class PresenceLabel extends React.Component {
     static propTypes = {
         // number of milliseconds ago this user was last active.
diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js
index ba2b3064fd..ade84cbef3 100644
--- a/src/components/views/rooms/ReadReceiptMarker.js
+++ b/src/components/views/rooms/ReadReceiptMarker.js
@@ -23,6 +23,7 @@ import {formatDate} from '../../../DateUtils';
 import Velociraptor from "../../../Velociraptor";
 import * as sdk from "../../../index";
 import {toPx} from "../../../utils/units";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 let bounce = false;
 try {
@@ -32,6 +33,7 @@ try {
 } catch (e) {
 }
 
+@replaceableComponent("views.rooms.ReadReceiptMarker")
 export default class ReadReceiptMarker extends React.PureComponent {
     static propTypes = {
         // the RoomMember to show the RR for
diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js
index c7872d95ed..0d99be4f53 100644
--- a/src/components/views/rooms/ReplyPreview.js
+++ b/src/components/views/rooms/ReplyPreview.js
@@ -23,6 +23,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import PropTypes from "prop-types";
 import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function cancelQuoting() {
     dis.dispatch({
@@ -31,6 +32,7 @@ function cancelQuoting() {
     });
 }
 
+@replaceableComponent("views.rooms.ReplyPreview")
 export default class ReplyPreview extends React.Component {
     static propTypes = {
         permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx
index ff60ab7779..ea0ff233da 100644
--- a/src/components/views/rooms/RoomBreadcrumbs.tsx
+++ b/src/components/views/rooms/RoomBreadcrumbs.tsx
@@ -27,6 +27,7 @@ import RoomListStore from "../../../stores/room-list/RoomListStore";
 import { DefaultTagID } from "../../../stores/room-list/models";
 import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
 import Toolbar from "../../../accessibility/Toolbar";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -42,6 +43,7 @@ interface IState {
     skipFirst: boolean;
 }
 
+@replaceableComponent("views.rooms.RoomBreadcrumbs")
 export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState> {
     private isMounted = true;
 
diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.js
index d8205aeb21..be22cda199 100644
--- a/src/components/views/rooms/RoomDetailList.js
+++ b/src/components/views/rooms/RoomDetailList.js
@@ -22,7 +22,9 @@ import PropTypes from 'prop-types';
 import classNames from 'classnames';
 
 import {roomShape} from './RoomDetailRow';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.RoomDetailList")
 export default class RoomDetailList extends React.Component {
     static propTypes = {
         rooms: PropTypes.arrayOf(roomShape),
diff --git a/src/components/views/rooms/RoomDetailRow.js b/src/components/views/rooms/RoomDetailRow.js
index 667f821922..e7c259cd98 100644
--- a/src/components/views/rooms/RoomDetailRow.js
+++ b/src/components/views/rooms/RoomDetailRow.js
@@ -21,6 +21,7 @@ import { linkifyElement } from '../../../HtmlUtils';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import PropTypes from 'prop-types';
 import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export function getDisplayAliasForRoom(room) {
     return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
@@ -39,6 +40,7 @@ export const roomShape = PropTypes.shape({
     guestCanJoin: PropTypes.bool,
 });
 
+@replaceableComponent("views.rooms.RoomDetailRow")
 export default class RoomDetailRow extends React.Component {
     static propTypes = {
         room: roomShape,
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 6736600bc8..f856f7f6ef 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -32,7 +32,9 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import RoomTopic from "../elements/RoomTopic";
 import RoomName from "../elements/RoomName";
 import {PlaceCallType} from "../../../CallHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.RoomHeader")
 export default class RoomHeader extends React.Component {
     static propTypes = {
         room: PropTypes.object,
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index f7da6571da..ff6e3793bf 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -50,6 +50,7 @@ import CallHandler from "../../../CallHandler";
 import SpaceStore from "../../../stores/SpaceStore";
 import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
 import { EventType } from "matrix-js-sdk/src/@types/event";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     onKeyDown: (ev: React.KeyboardEvent) => void;
@@ -256,6 +257,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
     };
 }
 
+@replaceableComponent("views.rooms.RoomList")
 export default class RoomList extends React.PureComponent<IProps, IState> {
     private dispatcherRef;
     private customTagStoreRef;
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js
index dc68068157..36038da61c 100644
--- a/src/components/views/rooms/RoomPreviewBar.js
+++ b/src/components/views/rooms/RoomPreviewBar.js
@@ -27,6 +27,7 @@ import SdkConfig from "../../../SdkConfig";
 import IdentityAuthClient from '../../../IdentityAuthClient';
 import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
 import {UPDATE_EVENT} from "../../../stores/AsyncStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const MessageCase = Object.freeze({
     NotLoggedIn: "NotLoggedIn",
@@ -45,6 +46,7 @@ const MessageCase = Object.freeze({
     OtherError: "OtherError",
 });
 
+@replaceableComponent("views.rooms.RoomPreviewBar")
 export default class RoomPreviewBar extends React.Component {
     static propTypes = {
         onJoinClick: PropTypes.func,
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index a2574bf60c..cb98ba85e4 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -51,6 +51,7 @@ import { objectExcluding, objectHasDiff } from "../../../utils/objects";
 import TemporaryTile from "./TemporaryTile";
 import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
 import IconizedContextMenu from "../context_menus/IconizedContextMenu";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
 const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
@@ -98,6 +99,7 @@ interface IState {
     filteredExtraTiles?: TemporaryTile[];
 }
 
+@replaceableComponent("views.rooms.RoomSublist")
 export default class RoomSublist extends React.Component<IProps, IState> {
     private headerButton = createRef<HTMLDivElement>();
     private sublistRef = createRef<HTMLDivElement>();
diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx
index 835447dc18..07de70fe45 100644
--- a/src/components/views/rooms/RoomTile.tsx
+++ b/src/components/views/rooms/RoomTile.tsx
@@ -51,6 +51,7 @@ import IconizedContextMenu, {
     IconizedContextMenuRadio,
 } from "../context_menus/IconizedContextMenu";
 import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     room: Room;
@@ -78,6 +79,7 @@ const contextMenuBelow = (elementRect: PartialDOMRect) => {
     return {left, top, chevronFace};
 };
 
+@replaceableComponent("views.rooms.RoomTile")
 export default class RoomTile extends React.PureComponent<IProps, IState> {
     private dispatcherRef: string;
     private roomTileRef = createRef<HTMLDivElement>();
diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js
index 877cfb39d7..a2d4f92d35 100644
--- a/src/components/views/rooms/RoomUpgradeWarningBar.js
+++ b/src/components/views/rooms/RoomUpgradeWarningBar.js
@@ -21,7 +21,9 @@ import Modal from '../../../Modal';
 
 import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.RoomUpgradeWarningBar")
 export default class RoomUpgradeWarningBar extends React.Component {
     static propTypes = {
         room: PropTypes.object.isRequired,
diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.js
index ac637673e4..029516c932 100644
--- a/src/components/views/rooms/SearchBar.js
+++ b/src/components/views/rooms/SearchBar.js
@@ -21,7 +21,9 @@ import classNames from "classnames";
 import { _t } from '../../../languageHandler';
 import {Key} from "../../../Keyboard";
 import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.SearchBar")
 export default class SearchBar extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js
index 29def9e368..dcfd633e76 100644
--- a/src/components/views/rooms/SearchResultTile.js
+++ b/src/components/views/rooms/SearchResultTile.js
@@ -21,7 +21,9 @@ import * as sdk from '../../../index';
 import {haveTileForEvent} from "./EventTile";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.SearchResultTile")
 export default class SearchResultTile extends React.Component {
     static propTypes = {
         // a matrix-js-sdk SearchResult containing the details of this result
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 673df949f7..ba3076c07d 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -48,6 +48,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import CountlyAnalytics from "../../../CountlyAnalytics";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import EMOJI_REGEX from 'emojibase-regex';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
     const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@@ -111,6 +112,7 @@ export function isQuickReaction(model) {
     return false;
 }
 
+@replaceableComponent("views.rooms.SendMessageComposer")
 export default class SendMessageComposer extends React.Component {
     static propTypes = {
         room: PropTypes.object.isRequired,
diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js
index 1c78253eff..b2a66f6670 100644
--- a/src/components/views/rooms/SimpleRoomHeader.js
+++ b/src/components/views/rooms/SimpleRoomHeader.js
@@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
 import AccessibleButton from '../elements/AccessibleButton';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // cancel button which is shared between room header and simple room header
 export function CancelButton(props) {
@@ -36,6 +37,7 @@ export function CancelButton(props) {
  * A stripped-down room header used for things like the user settings
  * and room directory.
  */
+@replaceableComponent("views.rooms.SimpleRoomHeader")
 export default class SimpleRoomHeader extends React.Component {
     static propTypes = {
         title: PropTypes.string,
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 5446d15671..44d31d7146 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -30,6 +30,7 @@ import {WidgetType} from "../../../widgets/WidgetType";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import {Action} from "../../../dispatcher/actions";
 import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
 // We sit in a context menu, so this should be given to the context menu.
@@ -38,6 +39,7 @@ const STICKERPICKER_Z_INDEX = 3500;
 // Key to store the widget's AppTile under in PersistedElement
 const PERSISTED_ELEMENT_KEY = "stickerPicker";
 
+@replaceableComponent("views.rooms.Stickerpicker")
 export default class Stickerpicker extends React.Component {
     static currentWidget;
 
diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx
index eec3105880..a9765faa5d 100644
--- a/src/components/views/rooms/TemporaryTile.tsx
+++ b/src/components/views/rooms/TemporaryTile.tsx
@@ -22,6 +22,7 @@ import {
 } from "../../../accessibility/RovingTabIndex";
 import NotificationBadge from "./NotificationBadge";
 import { NotificationState } from "../../../stores/notifications/NotificationState";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     isMinimized: boolean;
@@ -37,6 +38,7 @@ interface IState {
 }
 
 // TODO: Remove with community invites in the room list: https://github.com/vector-im/element-web/issues/14456
+@replaceableComponent("views.rooms.TemporaryTile")
 export default class TemporaryTile extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.js
index 73510c2b4f..5e2d82a1b2 100644
--- a/src/components/views/rooms/ThirdPartyMemberInfo.js
+++ b/src/components/views/rooms/ThirdPartyMemberInfo.js
@@ -25,7 +25,9 @@ import Modal from "../../../Modal";
 import {isValid3pidInvite} from "../../../RoomInvite";
 import RoomAvatar from "../avatars/RoomAvatar";
 import RoomName from "../elements/RoomName";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.ThirdPartyMemberInfo")
 export default class ThirdPartyMemberInfo extends React.Component {
     static propTypes = {
         event: PropTypes.instanceOf(MatrixEvent).isRequired,
diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.js
index 9ac3c49ef4..cba99ac913 100644
--- a/src/components/views/rooms/TopUnreadMessagesBar.js
+++ b/src/components/views/rooms/TopUnreadMessagesBar.js
@@ -20,7 +20,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { _t } from '../../../languageHandler';
 import AccessibleButton from '../elements/AccessibleButton';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.TopUnreadMessagesBar")
 export default class TopUnreadMessagesBar extends React.Component {
     static propTypes = {
         onScrollUpClick: PropTypes.func,
diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js
index 905cbe6d09..a25b43fc3a 100644
--- a/src/components/views/rooms/WhoIsTypingTile.js
+++ b/src/components/views/rooms/WhoIsTypingTile.js
@@ -21,7 +21,9 @@ import * as WhoIsTyping from '../../../WhoIsTyping';
 import Timer from '../../../utils/Timer';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import MemberAvatar from '../avatars/MemberAvatar';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.rooms.WhoIsTypingTile")
 export default class WhoIsTypingTile extends React.Component {
     static propTypes = {
         // the room this statusbar is representing.

From 41576954fd3dfbdff9604a76760e10db44c8f5b4 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 20:20:07 -0700
Subject: [PATCH 343/389] Batch of views getting replaceableComponent
 decorators

---
 src/components/views/settings/BridgeTile.tsx                   | 2 ++
 src/components/views/settings/ChangeAvatar.js                  | 2 ++
 src/components/views/settings/ChangeDisplayName.js             | 2 ++
 src/components/views/settings/ChangePassword.js                | 2 ++
 src/components/views/settings/CrossSigningPanel.js             | 2 ++
 src/components/views/settings/DevicesPanel.js                  | 2 ++
 src/components/views/settings/DevicesPanelEntry.js             | 2 ++
 src/components/views/settings/EventIndexPanel.js               | 2 ++
 src/components/views/settings/IntegrationManager.js            | 2 ++
 src/components/views/settings/Notifications.js                 | 2 ++
 src/components/views/settings/ProfileSettings.js               | 2 ++
 src/components/views/settings/SecureBackupPanel.js             | 2 ++
 src/components/views/settings/SetIdServer.js                   | 2 ++
 src/components/views/settings/SetIntegrationManager.js         | 2 ++
 src/components/views/settings/SpellCheckSettings.tsx           | 2 ++
 src/components/views/settings/account/EmailAddresses.js        | 2 ++
 src/components/views/settings/account/PhoneNumbers.js          | 2 ++
 src/components/views/settings/discovery/EmailAddresses.js      | 2 ++
 src/components/views/settings/discovery/PhoneNumbers.js        | 2 ++
 .../views/settings/tabs/room/AdvancedRoomSettingsTab.js        | 2 ++
 src/components/views/settings/tabs/room/BridgeSettingsTab.tsx  | 2 ++
 .../views/settings/tabs/room/GeneralRoomSettingsTab.js         | 2 ++
 .../views/settings/tabs/room/NotificationSettingsTab.js        | 2 ++
 .../views/settings/tabs/room/RolesRoomSettingsTab.js           | 2 ++
 .../views/settings/tabs/room/SecurityRoomSettingsTab.js        | 2 ++
 .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx     | 3 ++-
 .../views/settings/tabs/user/FlairUserSettingsTab.js           | 2 ++
 .../views/settings/tabs/user/GeneralUserSettingsTab.js         | 2 ++
 src/components/views/settings/tabs/user/HelpUserSettingsTab.js | 2 ++
 src/components/views/settings/tabs/user/LabsUserSettingsTab.js | 2 ++
 .../views/settings/tabs/user/MjolnirUserSettingsTab.js         | 2 ++
 .../views/settings/tabs/user/NotificationUserSettingsTab.js    | 2 ++
 .../views/settings/tabs/user/PreferencesUserSettingsTab.js     | 2 ++
 .../views/settings/tabs/user/SecurityUserSettingsTab.js        | 2 ++
 .../views/settings/tabs/user/VoiceUserSettingsTab.js           | 2 ++
 src/components/views/terms/InlineTermsAgreement.js             | 2 ++
 src/components/views/toasts/NonUrgentEchoFailureToast.tsx      | 2 ++
 src/components/views/toasts/VerificationRequestToast.tsx       | 2 ++
 src/components/views/verification/VerificationCancelled.js     | 2 ++
 src/components/views/verification/VerificationComplete.js      | 2 ++
 src/components/views/verification/VerificationShowSas.js       | 2 ++
 src/components/views/voip/CallContainer.tsx                    | 2 ++
 src/components/views/voip/CallPreview.tsx                      | 2 ++
 src/components/views/voip/CallView.tsx                         | 2 ++
 src/components/views/voip/CallViewForRoom.tsx                  | 2 ++
 src/components/views/voip/DialPad.tsx                          | 2 ++
 src/components/views/voip/DialPadModal.tsx                     | 2 ++
 src/components/views/voip/IncomingCallBox.tsx                  | 2 ++
 src/components/views/voip/VideoFeed.tsx                        | 2 ++
 49 files changed, 98 insertions(+), 1 deletion(-)

diff --git a/src/components/views/settings/BridgeTile.tsx b/src/components/views/settings/BridgeTile.tsx
index 58499ebd25..b33219ad4a 100644
--- a/src/components/views/settings/BridgeTile.tsx
+++ b/src/components/views/settings/BridgeTile.tsx
@@ -26,6 +26,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import {MatrixEvent} from "matrix-js-sdk/src/models/event";
 import { Room } from "matrix-js-sdk/src/models/room";
 import { isUrlPermitted } from '../../../HtmlUtils';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     ev: MatrixEvent;
@@ -64,6 +65,7 @@ interface IBridgeStateEvent {
     };
 }
 
+@replaceableComponent("views.settings.BridgeTile")
 export default class BridgeTile extends React.PureComponent<IProps> {
     static propTypes = {
         ev: PropTypes.object.isRequired,
diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js
index 7ab2936584..8067046ffd 100644
--- a/src/components/views/settings/ChangeAvatar.js
+++ b/src/components/views/settings/ChangeAvatar.js
@@ -20,7 +20,9 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import Spinner from '../elements/Spinner';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.ChangeAvatar")
 export default class ChangeAvatar extends React.Component {
     static propTypes = {
         initialAvatarUrl: PropTypes.string,
diff --git a/src/components/views/settings/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.js
index 538e52d0ca..cae4a22be9 100644
--- a/src/components/views/settings/ChangeDisplayName.js
+++ b/src/components/views/settings/ChangeDisplayName.js
@@ -20,7 +20,9 @@ import React from 'react';
 import * as sdk from '../../../index';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.ChangeDisplayName")
 export default class ChangeDisplayName extends React.Component {
     _getDisplayName = async () => {
         const cli = MatrixClientPeg.get();
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index 22b758b1ca..aa635ef974 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -27,6 +27,7 @@ import * as sdk from "../../../index";
 import Modal from "../../../Modal";
 import PassphraseField from "../auth/PassphraseField";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const FIELD_OLD_PASSWORD = 'field_old_password';
 const FIELD_NEW_PASSWORD = 'field_new_password';
@@ -34,6 +35,7 @@ const FIELD_NEW_PASSWORD_CONFIRM = 'field_new_password_confirm';
 
 const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
 
+@replaceableComponent("views.settings.ChangePassword")
 export default class ChangePassword extends React.Component {
     static propTypes = {
         onFinished: PropTypes.func,
diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js
index 1c548bd9d8..e5f57d1af2 100644
--- a/src/components/views/settings/CrossSigningPanel.js
+++ b/src/components/views/settings/CrossSigningPanel.js
@@ -23,7 +23,9 @@ import Modal from '../../../Modal';
 import Spinner from '../elements/Spinner';
 import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog';
 import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.CrossSigningPanel")
 export default class CrossSigningPanel extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js
index dc3ce9e03d..e7d300b0f8 100644
--- a/src/components/views/settings/DevicesPanel.js
+++ b/src/components/views/settings/DevicesPanel.js
@@ -24,7 +24,9 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { _t } from '../../../languageHandler';
 import Modal from '../../../Modal';
 import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.DevicesPanel")
 export default class DevicesPanel extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js
index 567b144a92..93d4c78476 100644
--- a/src/components/views/settings/DevicesPanelEntry.js
+++ b/src/components/views/settings/DevicesPanelEntry.js
@@ -22,7 +22,9 @@ import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import {formatDate} from '../../../DateUtils';
 import StyledCheckbox from '../elements/StyledCheckbox';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.DevicesPanelEntry")
 export default class DevicesPanelEntry extends React.Component {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.js
index ec6ccacc9a..d78b99fc5d 100644
--- a/src/components/views/settings/EventIndexPanel.js
+++ b/src/components/views/settings/EventIndexPanel.js
@@ -25,7 +25,9 @@ import AccessibleButton from "../elements/AccessibleButton";
 import {formatBytes, formatCountLong} from "../../../utils/FormattingUtils";
 import EventIndexPeg from "../../../indexing/EventIndexPeg";
 import {SettingLevel} from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.EventIndexPanel")
 export default class EventIndexPanel extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/IntegrationManager.js b/src/components/views/settings/IntegrationManager.js
index da11832cf5..b058625139 100644
--- a/src/components/views/settings/IntegrationManager.js
+++ b/src/components/views/settings/IntegrationManager.js
@@ -21,7 +21,9 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import dis from '../../../dispatcher/dispatcher';
 import {Key} from "../../../Keyboard";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.IntegrationManager")
 export default class IntegrationManager extends React.Component {
     static propTypes = {
         // false to display an error saying that we couldn't connect to the integration manager
diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js
index 1337991dc3..25fe434994 100644
--- a/src/components/views/settings/Notifications.js
+++ b/src/components/views/settings/Notifications.js
@@ -32,6 +32,7 @@ import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import AccessibleButton from "../elements/AccessibleButton";
 import {SettingLevel} from "../../../settings/SettingLevel";
 import {UIFeature} from "../../../settings/UIFeature";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // TODO: this "view" component still has far too much application logic in it,
 // which should be factored out to other files.
@@ -65,6 +66,7 @@ function portLegacyActions(actions) {
     }
 }
 
+@replaceableComponent("views.settings.Notifications")
 export default class Notifications extends React.Component {
     static phases = {
         LOADING: "LOADING", // The component is loading or sending data to the hs
diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 89d7cf6c2b..30dcdc3c47 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -23,7 +23,9 @@ import * as sdk from "../../../index";
 import {OwnProfileStore} from "../../../stores/OwnProfileStore";
 import Modal from "../../../Modal";
 import ErrorDialog from "../dialogs/ErrorDialog";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.ProfileSettings")
 export default class ProfileSettings extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js
index 080d83b2cf..310114c8af 100644
--- a/src/components/views/settings/SecureBackupPanel.js
+++ b/src/components/views/settings/SecureBackupPanel.js
@@ -26,7 +26,9 @@ import AccessibleButton from '../elements/AccessibleButton';
 import QuestionDialog from '../dialogs/QuestionDialog';
 import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
 import { accessSecretStorage } from '../../../SecurityManager';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.SecureBackupPanel")
 export default class SecureBackupPanel extends React.PureComponent {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.js
index e05fe4f1c3..fa2a36476d 100644
--- a/src/components/views/settings/SetIdServer.js
+++ b/src/components/views/settings/SetIdServer.js
@@ -27,6 +27,7 @@ import IdentityAuthClient from "../../../IdentityAuthClient";
 import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
 import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
 import {timeout} from "../../../utils/promise";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // We'll wait up to this long when checking for 3PID bindings on the IS.
 const REACHABILITY_TIMEOUT = 10000; // ms
@@ -58,6 +59,7 @@ async function checkIdentityServerUrl(u) {
     }
 }
 
+@replaceableComponent("views.settings.SetIdServer")
 export default class SetIdServer extends React.Component {
     static propTypes = {
         // Whether or not the ID server is missing terms. This affects the text
diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js
index e6fb3f6e1c..29cc5d7131 100644
--- a/src/components/views/settings/SetIntegrationManager.js
+++ b/src/components/views/settings/SetIntegrationManager.js
@@ -20,7 +20,9 @@ import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
 import * as sdk from '../../../index';
 import SettingsStore from "../../../settings/SettingsStore";
 import {SettingLevel} from "../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.SetIntegrationManager")
 export default class SetIntegrationManager extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx
index d08f263b5f..e5455c8c68 100644
--- a/src/components/views/settings/SpellCheckSettings.tsx
+++ b/src/components/views/settings/SpellCheckSettings.tsx
@@ -18,6 +18,7 @@ import React from 'react';
 import SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
 import AccessibleButton from "../../../components/views/elements/AccessibleButton";
 import {_t} from "../../../languageHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface ExistingSpellCheckLanguageIProps {
     language: string,
@@ -53,6 +54,7 @@ export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellChe
     }
 }
 
+@replaceableComponent("views.settings.SpellCheckLanguages")
 export default class SpellCheckLanguages extends React.Component<SpellCheckLanguagesIProps, SpellCheckLanguagesIState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js
index a8de7693a9..1ebd374173 100644
--- a/src/components/views/settings/account/EmailAddresses.js
+++ b/src/components/views/settings/account/EmailAddresses.js
@@ -25,6 +25,7 @@ import * as Email from "../../../../email";
 import AddThreepid from "../../../../AddThreepid";
 import * as sdk from '../../../../index';
 import Modal from '../../../../Modal';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
 TODO: Improve the UX for everything in here.
@@ -112,6 +113,7 @@ export class ExistingEmailAddress extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.account.EmailAddresses")
 export default class EmailAddresses extends React.Component {
     static propTypes = {
         emails: PropTypes.array.isRequired,
diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js
index df54b5ca1f..5725fdb909 100644
--- a/src/components/views/settings/account/PhoneNumbers.js
+++ b/src/components/views/settings/account/PhoneNumbers.js
@@ -25,6 +25,7 @@ import AddThreepid from "../../../../AddThreepid";
 import CountryDropdown from "../../auth/CountryDropdown";
 import * as sdk from '../../../../index';
 import Modal from '../../../../Modal';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
 TODO: Improve the UX for everything in here.
@@ -107,6 +108,7 @@ export class ExistingPhoneNumber extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.account.PhoneNumbers")
 export default class PhoneNumbers extends React.Component {
     static propTypes = {
         msisdns: PropTypes.array.isRequired,
diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js
index f9a1ba1818..0493597537 100644
--- a/src/components/views/settings/discovery/EmailAddresses.js
+++ b/src/components/views/settings/discovery/EmailAddresses.js
@@ -23,6 +23,7 @@ import {MatrixClientPeg} from "../../../../MatrixClientPeg";
 import * as sdk from '../../../../index';
 import Modal from '../../../../Modal';
 import AddThreepid from '../../../../AddThreepid';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
 TODO: Improve the UX for everything in here.
@@ -233,6 +234,7 @@ export class EmailAddress extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.discovery.EmailAddresses")
 export default class EmailAddresses extends React.Component {
     static propTypes = {
         emails: PropTypes.array.isRequired,
diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js
index 03f459ee15..5cbcdfe47e 100644
--- a/src/components/views/settings/discovery/PhoneNumbers.js
+++ b/src/components/views/settings/discovery/PhoneNumbers.js
@@ -23,6 +23,7 @@ import {MatrixClientPeg} from "../../../../MatrixClientPeg";
 import * as sdk from '../../../../index';
 import Modal from '../../../../Modal';
 import AddThreepid from '../../../../AddThreepid';
+import {replaceableComponent} from "../../../../utils/replaceableComponent";
 
 /*
 TODO: Improve the UX for everything in here.
@@ -246,6 +247,7 @@ export class PhoneNumber extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.discovery.PhoneNumbers")
 export default class PhoneNumbers extends React.Component {
     static propTypes = {
         msisdns: PropTypes.array.isRequired,
diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
index 2fa61a0ee6..28aad65129 100644
--- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
@@ -22,7 +22,9 @@ import * as sdk from "../../../../..";
 import AccessibleButton from "../../../elements/AccessibleButton";
 import Modal from "../../../../../Modal";
 import dis from "../../../../../dispatcher/dispatcher";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
 export default class AdvancedRoomSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
index 3c74bd4c1a..8d886a191e 100644
--- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
@@ -21,6 +21,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event";
 import {_t} from "../../../../../languageHandler";
 import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
 import BridgeTile from "../../BridgeTile";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 const BRIDGE_EVENT_TYPES = [
     "uk.half-shot.bridge",
@@ -33,6 +34,7 @@ interface IProps {
     roomId: string;
 }
 
+@replaceableComponent("views.settings.tabs.room.BridgeSettingsTab")
 export default class BridgeSettingsTab extends React.Component<IProps> {
     private renderBridgeCard(event: MatrixEvent, room: Room) {
         const content = event.getContent();
diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
index 9b8004d9d6..cd4a043622 100644
--- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
@@ -24,7 +24,9 @@ import dis from "../../../../../dispatcher/dispatcher";
 import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
 import SettingsStore from "../../../../../settings/SettingsStore";
 import {UIFeature} from "../../../../../settings/UIFeature";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.room.GeneralRoomSettingsTab")
 export default class GeneralRoomSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js
index dd88b5018f..baefb5ae20 100644
--- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js
+++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js
@@ -22,7 +22,9 @@ import AccessibleButton from "../../../elements/AccessibleButton";
 import Notifier from "../../../../../Notifier";
 import SettingsStore from '../../../../../settings/SettingsStore';
 import {SettingLevel} from "../../../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.room.NotificationsSettingsTab")
 export default class NotificationsSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
index 49d683c42a..09498e0d4a 100644
--- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
@@ -21,6 +21,7 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
 import * as sdk from "../../../../..";
 import AccessibleButton from "../../../elements/AccessibleButton";
 import Modal from "../../../../../Modal";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 const plEventsToLabels = {
     // These will be translated for us later.
@@ -103,6 +104,7 @@ export class BannedUser extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab")
 export default class RolesRoomSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
index f72e78fa3f..ce883c6d23 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
@@ -26,7 +26,9 @@ import StyledRadioGroup from '../../../elements/StyledRadioGroup';
 import {SettingLevel} from "../../../../../settings/SettingLevel";
 import SettingsStore from "../../../../../settings/SettingsStore";
 import {UIFeature} from "../../../../../settings/UIFeature";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab")
 export default class SecurityRoomSettingsTab extends React.Component {
     static propTypes = {
         roomId: PropTypes.string.isRequired,
diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
index 80a20d8afa..d6e01d194c 100644
--- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
@@ -36,6 +36,7 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
 import { SettingLevel } from "../../../../../settings/SettingLevel";
 import {UIFeature} from "../../../../../settings/UIFeature";
 import {Layout} from "../../../../../settings/Layout";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -64,7 +65,7 @@ interface IState extends IThemeState {
     layout: Layout;
 }
 
-
+@replaceableComponent("views.settings.tabs.user.AppearanceUserSettingsTab")
 export default class AppearanceUserSettingsTab extends React.Component<IProps, IState> {
     private readonly MESSAGE_PREVIEW_TEXT = _t("Hey you. You're the best!");
 
diff --git a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
index 26e0033233..28e80f3030 100644
--- a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import {_t} from "../../../../../languageHandler";
 import GroupUserSettings from "../../../groups/GroupUserSettings";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.FlairUserSettingsTab")
 export default class FlairUserSettingsTab extends React.Component {
     render() {
         return (
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index b17ab18c39..314acf5d65 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -39,7 +39,9 @@ import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
 import Spinner from "../../../elements/Spinner";
 import {SettingLevel} from "../../../../../settings/SettingLevel";
 import {UIFeature} from "../../../../../settings/UIFeature";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.GeneralUserSettingsTab")
 export default class GeneralUserSettingsTab extends React.Component {
     static propTypes = {
         closeSettingsFn: PropTypes.func.isRequired,
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
index 85ba22a353..e16ee686f5 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
@@ -27,7 +27,9 @@ import * as sdk from "../../../../../";
 import PlatformPeg from "../../../../../PlatformPeg";
 import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
 import UpdateCheckButton from "../../UpdateCheckButton";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab")
 export default class HelpUserSettingsTab extends React.Component {
     static propTypes = {
         closeSettingsFn: PropTypes.func.isRequired,
diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
index 91bc9abcad..f515f1862b 100644
--- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
@@ -21,6 +21,7 @@ import SettingsStore from "../../../../../settings/SettingsStore";
 import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
 import * as sdk from "../../../../../index";
 import {SettingLevel} from "../../../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 export class LabsSettingToggle extends React.Component {
     static propTypes = {
@@ -40,6 +41,7 @@ export class LabsSettingToggle extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.tabs.user.LabsUserSettingsTab")
 export default class LabsUserSettingsTab extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js
index 510d6076a0..91f6728a7a 100644
--- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js
@@ -23,7 +23,9 @@ import {BanList, RULE_SERVER, RULE_USER} from "../../../../../mjolnir/BanList";
 import Modal from "../../../../../Modal";
 import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
 import * as sdk from "../../../../../index";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.MjolnirUserSettingsTab")
 export default class MjolnirUserSettingsTab extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js
index 2e649cb7f8..8a71d1bf15 100644
--- a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js
@@ -17,7 +17,9 @@ limitations under the License.
 import React from 'react';
 import {_t} from "../../../../../languageHandler";
 import * as sdk from "../../../../../index";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab")
 export default class NotificationUserSettingsTab extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index ae9cad4cfa..238f875e22 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -23,7 +23,9 @@ import Field from "../../../elements/Field";
 import * as sdk from "../../../../..";
 import PlatformPeg from "../../../../../PlatformPeg";
 import {SettingLevel} from "../../../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab")
 export default class PreferencesUserSettingsTab extends React.Component {
     static ROOM_LIST_SETTINGS = [
         'breadcrumbs',
diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
index a0d9016ce2..8a70811399 100644
--- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
@@ -34,6 +34,7 @@ import SettingsStore from "../../../../../settings/SettingsStore";
 import {UIFeature} from "../../../../../settings/UIFeature";
 import {isE2eAdvancedPanelPossible} from "../../E2eAdvancedPanel";
 import CountlyAnalytics from "../../../../../CountlyAnalytics";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
 export class IgnoredUser extends React.Component {
     static propTypes = {
@@ -59,6 +60,7 @@ export class IgnoredUser extends React.Component {
     }
 }
 
+@replaceableComponent("views.settings.tabs.user.SecurityUserSettingsTab")
 export default class SecurityUserSettingsTab extends React.Component {
     static propTypes = {
         closeSettingsFn: PropTypes.func.isRequired,
diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
index a78cc10b92..bc6fe796b8 100644
--- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
@@ -25,7 +25,9 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
 import * as sdk from "../../../../../index";
 import Modal from "../../../../../Modal";
 import {SettingLevel} from "../../../../../settings/SettingLevel";
+import {replaceableComponent} from "../../../../../utils/replaceableComponent";
 
+@replaceableComponent("views.settings.tabs.user.VoiceUserSettingsTab")
 export default class VoiceUserSettingsTab extends React.Component {
     constructor() {
         super();
diff --git a/src/components/views/terms/InlineTermsAgreement.js b/src/components/views/terms/InlineTermsAgreement.js
index 5f6e276976..473a97642c 100644
--- a/src/components/views/terms/InlineTermsAgreement.js
+++ b/src/components/views/terms/InlineTermsAgreement.js
@@ -20,7 +20,9 @@ import {_t, pickBestLanguage} from "../../../languageHandler";
 import * as sdk from "../../..";
 import {objectClone} from "../../../utils/objects";
 import StyledCheckbox from "../elements/StyledCheckbox";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.terms.InlineTermsAgreement")
 export default class InlineTermsAgreement extends React.Component {
     static propTypes = {
         policiesAndServicePairs: PropTypes.array.isRequired, // array of service/policy pairs
diff --git a/src/components/views/toasts/NonUrgentEchoFailureToast.tsx b/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
index 76d0328e8b..abf5b10692 100644
--- a/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
+++ b/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
@@ -19,7 +19,9 @@ import { _t } from "../../../languageHandler";
 import AccessibleButton from "../elements/AccessibleButton";
 import Modal from "../../../Modal";
 import ServerOfflineDialog from "../dialogs/ServerOfflineDialog";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.toasts.NonUrgentEchoFailureToast")
 export default class NonUrgentEchoFailureToast extends React.PureComponent {
     private openDialog = () => {
         Modal.createTrackedDialog('Local Echo Server Error', '', ServerOfflineDialog, {});
diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx
index 8c8a74b2be..010c8fd12f 100644
--- a/src/components/views/toasts/VerificationRequestToast.tsx
+++ b/src/components/views/toasts/VerificationRequestToast.tsx
@@ -29,6 +29,7 @@ import GenericToast from "./GenericToast";
 import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 import {DeviceInfo} from "matrix-js-sdk/src/crypto/deviceinfo";
 import {Action} from "../../../dispatcher/actions";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     toastKey: string;
@@ -40,6 +41,7 @@ interface IState {
     device?: DeviceInfo;
 }
 
+@replaceableComponent("views.toasts.VerificationRequestToast")
 export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
     private intervalHandle: NodeJS.Timeout;
 
diff --git a/src/components/views/verification/VerificationCancelled.js b/src/components/views/verification/VerificationCancelled.js
index fc2a287359..0bbaea1804 100644
--- a/src/components/views/verification/VerificationCancelled.js
+++ b/src/components/views/verification/VerificationCancelled.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.verification.VerificationCancelled")
 export default class VerificationCancelled extends React.Component {
     static propTypes = {
         onDone: PropTypes.func.isRequired,
diff --git a/src/components/views/verification/VerificationComplete.js b/src/components/views/verification/VerificationComplete.js
index 2214711b1f..cf2a72591c 100644
--- a/src/components/views/verification/VerificationComplete.js
+++ b/src/components/views/verification/VerificationComplete.js
@@ -18,7 +18,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
+@replaceableComponent("views.verification.VerificationComplete")
 export default class VerificationComplete extends React.Component {
     static propTypes = {
         onDone: PropTypes.func.isRequired,
diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js
index 09374b91af..36f99b2140 100644
--- a/src/components/views/verification/VerificationShowSas.js
+++ b/src/components/views/verification/VerificationShowSas.js
@@ -21,11 +21,13 @@ import {PendingActionSpinner} from "../right_panel/EncryptionInfo";
 import AccessibleButton from "../elements/AccessibleButton";
 import DialogButtons from "../elements/DialogButtons";
 import { fixupColorFonts } from '../../../utils/FontManager';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 function capFirst(s) {
     return s.charAt(0).toUpperCase() + s.slice(1);
 }
 
+@replaceableComponent("views.verification.VerificationShowSas")
 export default class VerificationShowSas extends React.Component {
     static propTypes = {
         pending: PropTypes.bool,
diff --git a/src/components/views/voip/CallContainer.tsx b/src/components/views/voip/CallContainer.tsx
index 51925cb147..9d0047fc54 100644
--- a/src/components/views/voip/CallContainer.tsx
+++ b/src/components/views/voip/CallContainer.tsx
@@ -17,6 +17,7 @@ limitations under the License.
 import React from 'react';
 import IncomingCallBox from './IncomingCallBox';
 import CallPreview from './CallPreview';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
 
@@ -26,6 +27,7 @@ interface IState {
 
 }
 
+@replaceableComponent("views.voip.CallContainer")
 export default class CallContainer extends React.PureComponent<IProps, IState> {
     public render() {
         return <div className="mx_CallContainer">
diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx
index c08e52181b..29de068b0c 100644
--- a/src/components/views/voip/CallPreview.tsx
+++ b/src/components/views/voip/CallPreview.tsx
@@ -26,6 +26,7 @@ import PersistentApp from "../elements/PersistentApp";
 import SettingsStore from "../../../settings/SettingsStore";
 import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const SHOW_CALL_IN_STATES = [
     CallState.Connected,
@@ -85,6 +86,7 @@ function getPrimarySecondaryCalls(calls: MatrixCall[]): [MatrixCall, MatrixCall[
  * CallPreview shows a small version of CallView hovering over the UI in 'picture-in-picture'
  * (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing.
  */
+@replaceableComponent("views.voip.CallPreview")
 export default class CallPreview extends React.Component<IProps, IState> {
     private roomStoreToken: any;
     private dispatcherRef: string;
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 7cac682794..9bdc8fb11d 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -31,6 +31,7 @@ import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} f
 import CallContextMenu from '../context_menus/CallContextMenu';
 import { avatarUrlForMember } from '../../../Avatar';
 import DialpadContextMenu from '../context_menus/DialpadContextMenu';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
         // The call for us to display
@@ -100,6 +101,7 @@ const BOTTOM_PADDING = 10;
 const BOTTOM_MARGIN_TOP_BOTTOM = 10; // top margin plus bottom margin
 const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
 
+@replaceableComponent("views.voip.CallView")
 export default class CallView extends React.Component<IProps, IState> {
     private dispatcherRef: string;
     private contentRef = createRef<HTMLDivElement>();
diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx
index 4cb4e66fbe..97960d1e0b 100644
--- a/src/components/views/voip/CallViewForRoom.tsx
+++ b/src/components/views/voip/CallViewForRoom.tsx
@@ -19,6 +19,7 @@ import React from 'react';
 import CallHandler from '../../../CallHandler';
 import CallView from './CallView';
 import dis from '../../../dispatcher/dispatcher';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     // What room we should display the call for
@@ -40,6 +41,7 @@ interface IState {
  * Wrapper for CallView that always display the call in a given room,
  * or nothing if there is no call in that room.
  */
+@replaceableComponent("views.voip.CallViewForRoom")
 export default class CallViewForRoom extends React.Component<IProps, IState> {
     private dispatcherRef: string;
 
diff --git a/src/components/views/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx
index da88f49adf..3598f511f5 100644
--- a/src/components/views/voip/DialPad.tsx
+++ b/src/components/views/voip/DialPad.tsx
@@ -16,6 +16,7 @@ limitations under the License.
 
 import * as React from "react";
 import AccessibleButton from "../elements/AccessibleButton";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 const BUTTONS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
 
@@ -59,6 +60,7 @@ interface IProps {
     onDialPress?: (string) => void;
 }
 
+@replaceableComponent("views.voip.Dialpad")
 export default class Dialpad extends React.PureComponent<IProps> {
     render() {
         const buttonNodes = [];
diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx
index 9f031a48a3..c085809d5a 100644
--- a/src/components/views/voip/DialPadModal.tsx
+++ b/src/components/views/voip/DialPadModal.tsx
@@ -25,6 +25,7 @@ import dis from '../../../dispatcher/dispatcher';
 import Modal from "../../../Modal";
 import ErrorDialog from "../../views/dialogs/ErrorDialog";
 import CallHandler from "../../../CallHandler";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
     onFinished: (boolean) => void;
@@ -34,6 +35,7 @@ interface IState {
     value: string;
 }
 
+@replaceableComponent("views.voip.DialpadModal")
 export default class DialpadModal extends React.PureComponent<IProps, IState> {
     constructor(props) {
         super(props);
diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx
index a495093d85..0ca2a196c2 100644
--- a/src/components/views/voip/IncomingCallBox.tsx
+++ b/src/components/views/voip/IncomingCallBox.tsx
@@ -25,6 +25,7 @@ import CallHandler from '../../../CallHandler';
 import RoomAvatar from '../avatars/RoomAvatar';
 import FormButton from '../elements/FormButton';
 import { CallState } from 'matrix-js-sdk/src/webrtc/call';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface IProps {
 }
@@ -33,6 +34,7 @@ interface IState {
     incomingCall: any;
 }
 
+@replaceableComponent("views.voip.IncomingCallBox")
 export default class IncomingCallBox extends React.Component<IProps, IState> {
     private dispatcherRef: string;
 
diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx
index 5210f28eb1..23dbe4d46b 100644
--- a/src/components/views/voip/VideoFeed.tsx
+++ b/src/components/views/voip/VideoFeed.tsx
@@ -18,6 +18,7 @@ import classnames from 'classnames';
 import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import React, {createRef} from 'react';
 import SettingsStore from "../../../settings/SettingsStore";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 export enum VideoFeedType {
     Local,
@@ -37,6 +38,7 @@ interface IProps {
     onResize?: (e: Event) => void,
 }
 
+@replaceableComponent("views.voip.VideoFeed")
 export default class VideoFeed extends React.Component<IProps> {
     private vid = createRef<HTMLVideoElement>();
 

From 591ccabab91e0ab5d30cc23acaca56c01c57858a Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 20:26:08 -0700
Subject: [PATCH 344/389] Remove a bunch of useless 'use strict' definitions

---
 src/components/structures/EmbeddedPage.js         | 2 --
 src/components/views/auth/AuthBody.js             | 2 --
 src/components/views/auth/AuthHeaderLogo.js       | 2 --
 src/components/views/auth/CompleteSecurityBody.js | 2 --
 src/components/views/elements/Flair.js            | 2 --
 src/components/views/messages/MAudioBody.js       | 2 --
 src/notifications/VectorPushRulesDefinitions.js   | 2 --
 src/notifications/index.js                        | 2 --
 src/utils/MegolmExportEncryption.js               | 2 --
 test/test-utils.js                                | 2 --
 test/utils/MegolmExportEncryption-test.js         | 2 --
 11 files changed, 22 deletions(-)

diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js
index cbfeff7582..c37ab3df48 100644
--- a/src/components/structures/EmbeddedPage.js
+++ b/src/components/structures/EmbeddedPage.js
@@ -16,8 +16,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-'use strict';
-
 import React from 'react';
 import PropTypes from 'prop-types';
 import request from 'browser-request';
diff --git a/src/components/views/auth/AuthBody.js b/src/components/views/auth/AuthBody.js
index 9a078efb52..7881486a5f 100644
--- a/src/components/views/auth/AuthBody.js
+++ b/src/components/views/auth/AuthBody.js
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-'use strict';
-
 import React from 'react';
 
 export default class AuthBody extends React.PureComponent {
diff --git a/src/components/views/auth/AuthHeaderLogo.js b/src/components/views/auth/AuthHeaderLogo.js
index 9edf149a83..2f27885322 100644
--- a/src/components/views/auth/AuthHeaderLogo.js
+++ b/src/components/views/auth/AuthHeaderLogo.js
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-'use strict';
-
 import React from 'react';
 
 export default class AuthHeaderLogo extends React.PureComponent {
diff --git a/src/components/views/auth/CompleteSecurityBody.js b/src/components/views/auth/CompleteSecurityBody.js
index d757de9fe0..734af3192c 100644
--- a/src/components/views/auth/CompleteSecurityBody.js
+++ b/src/components/views/auth/CompleteSecurityBody.js
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-'use strict';
-
 import React from 'react';
 
 export default class CompleteSecurityBody extends React.PureComponent {
diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js
index 0f06904b68..645444b300 100644
--- a/src/components/views/elements/Flair.js
+++ b/src/components/views/elements/Flair.js
@@ -14,8 +14,6 @@
  limitations under the License.
  */
 
-'use strict';
-
 import React from 'react';
 import PropTypes from 'prop-types';
 import FlairStore from '../../../stores/FlairStore';
diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js
index 587dee4513..ac42b485d7 100644
--- a/src/components/views/messages/MAudioBody.js
+++ b/src/components/views/messages/MAudioBody.js
@@ -14,8 +14,6 @@
  limitations under the License.
  */
 
-'use strict';
-
 import React from 'react';
 import MFileBody from './MFileBody';
 
diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js
index 98d197a004..c049a81c15 100644
--- a/src/notifications/VectorPushRulesDefinitions.js
+++ b/src/notifications/VectorPushRulesDefinitions.js
@@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-'use strict';
-
 import { _td } from '../languageHandler';
 import {StandardActions} from "./StandardActions";
 import {PushRuleVectorState} from "./PushRuleVectorState";
diff --git a/src/notifications/index.js b/src/notifications/index.js
index 7c400ad8b3..96c176303b 100644
--- a/src/notifications/index.js
+++ b/src/notifications/index.js
@@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-'use strict';
-
 export * from "./NotificationUtils";
 export * from "./PushRuleVectorState";
 export * from "./VectorPushRulesDefinitions";
diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js
index cde20a0eb2..be7472901a 100644
--- a/src/utils/MegolmExportEncryption.js
+++ b/src/utils/MegolmExportEncryption.js
@@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-"use strict";
-
 // polyfill textencoder if necessary
 import * as TextEncodingUtf8 from 'text-encoding-utf-8';
 let TextEncoder = window.TextEncoder;
diff --git a/test/test-utils.js b/test/test-utils.js
index 839e1d6cd8..b6e0468d86 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -1,5 +1,3 @@
-"use strict";
-
 import React from 'react';
 import {MatrixClientPeg as peg} from '../src/MatrixClientPeg';
 import dis from '../src/dispatcher/dispatcher';
diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js
index 1fd305b0a6..e0ed5ba26a 100644
--- a/test/utils/MegolmExportEncryption-test.js
+++ b/test/utils/MegolmExportEncryption-test.js
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-"use strict";
-
 import {TextEncoder} from "util";
 import nodeCrypto from "crypto";
 import { Crypto } from "@peculiar/webcrypto";

From 81e1f36c4b3a0b5a260da20f9b84e91083d4f302 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 8 Mar 2021 17:48:08 +0000
Subject: [PATCH 345/389] Tidy up TemporaryTile now that it isn't temporary

---
 .../views/avatars/_DecoratedRoomAvatar.scss   |  3 +--
 .../{TemporaryTile.tsx => ExtraTile.tsx}      |  7 +++--
 src/components/views/rooms/RoomList.tsx       | 14 +++++-----
 src/components/views/rooms/RoomSublist.tsx    | 26 +++++++++----------
 4 files changed, 23 insertions(+), 27 deletions(-)
 rename src/components/views/rooms/{TemporaryTile.tsx => ExtraTile.tsx} (92%)

diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss
index e0afd9de66..2631cbfb40 100644
--- a/res/css/views/avatars/_DecoratedRoomAvatar.scss
+++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-// XXX: We shouldn't be using TemporaryTile anywhere - delete it.
-.mx_DecoratedRoomAvatar, .mx_TemporaryTile {
+.mx_DecoratedRoomAvatar, .mx_ExtraTile {
     position: relative;
 
     &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {
diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/ExtraTile.tsx
similarity index 92%
rename from src/components/views/rooms/TemporaryTile.tsx
rename to src/components/views/rooms/ExtraTile.tsx
index 31d2acbc61..20d12955d5 100644
--- a/src/components/views/rooms/TemporaryTile.tsx
+++ b/src/components/views/rooms/ExtraTile.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -36,8 +36,7 @@ interface IState {
     hover: boolean;
 }
 
-// TODO: Remove with community invites in the room list: https://github.com/vector-im/element-web/issues/14456
-export default class TemporaryTile extends React.Component<IProps, IState> {
+export default class ExtraTile extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
 
@@ -57,8 +56,8 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
     public render(): React.ReactElement {
         // XXX: We copy classes because it's easier
         const classes = classNames({
+            'mx_ExtraTile': true,
             'mx_RoomTile': true,
-            'mx_TemporaryTile': true,
             'mx_RoomTile_selected': this.props.isSelected,
             'mx_RoomTile_minimized': this.props.isMinimized,
         });
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index beb85e50ce..7b44647be6 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import * as React from "react";
+import React, { ReactComponentElement } from "react";
 import { Dispatcher } from "flux";
 import { Room } from "matrix-js-sdk/src/models/room";
 import * as fbEmitter from "fbemitter";
@@ -34,7 +34,7 @@ import RoomSublist from "./RoomSublist";
 import { ActionPayload } from "../../../dispatcher/payloads";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import GroupAvatar from "../avatars/GroupAvatar";
-import TemporaryTile from "./TemporaryTile";
+import ExtraTile from "./ExtraTile";
 import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
 import { NotificationColor } from "../../../stores/notifications/NotificationColor";
 import { Action } from "../../../dispatcher/actions";
@@ -422,7 +422,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
         dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
     };
 
-    private renderSuggestedRooms(): JSX.Element[] {
+    private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
         return this.state.suggestedRooms.map(room => {
             const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
             const avatar = (
@@ -443,7 +443,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
                 });
             };
             return (
-                <TemporaryTile
+                <ExtraTile
                     isMinimized={this.props.isMinimized}
                     isSelected={this.state.currentRoomId === room.room_id}
                     displayName={name}
@@ -455,7 +455,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
         });
     }
 
-    private renderCommunityInvites(): TemporaryTile[] {
+    private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
         // TODO: Put community invites in a more sensible place (not in the room list)
         // See https://github.com/vector-im/element-web/issues/14456
         return MatrixClientPeg.get().getGroups().filter(g => {
@@ -476,7 +476,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
                 });
             };
             return (
-                <TemporaryTile
+                <ExtraTile
                     isMinimized={this.props.isMinimized}
                     isSelected={false}
                     displayName={g.name}
@@ -538,7 +538,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
                 isMinimized={this.props.isMinimized}
                 onResize={this.props.onResize}
                 showSkeleton={showSkeleton}
-                extraBadTilesThatShouldntExist={extraTiles}
+                extraTiles={extraTiles}
             />);
         }
 
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index a2574bf60c..1a9ff182bc 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -17,7 +17,7 @@ limitations under the License.
 */
 
 import * as React from "react";
-import {createRef} from "react";
+import { createRef, ReactComponentElement } from "react";
 import { Room } from "matrix-js-sdk/src/models/room";
 import classNames from 'classnames';
 import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
@@ -48,7 +48,7 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
 import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
 import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
 import { objectExcluding, objectHasDiff } from "../../../utils/objects";
-import TemporaryTile from "./TemporaryTile";
+import ExtraTile from "./ExtraTile";
 import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
 import IconizedContextMenu from "../context_menus/IconizedContextMenu";
 
@@ -73,9 +73,7 @@ interface IProps {
     onResize: () => void;
     showSkeleton?: boolean;
 
-    // TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
-    // You should feel bad if you use this.
-    extraBadTilesThatShouldntExist?: TemporaryTile[];
+    extraTiles?: ReactComponentElement<typeof ExtraTile>[];
 
     // TODO: Account for https://github.com/vector-im/element-web/issues/14179
 }
@@ -95,7 +93,7 @@ interface IState {
     isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
     height: number;
     rooms: Room[];
-    filteredExtraTiles?: TemporaryTile[];
+    filteredExtraTiles?: ReactComponentElement<typeof ExtraTile>[];
 }
 
 export default class RoomSublist extends React.Component<IProps, IState> {
@@ -153,12 +151,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
         return padding;
     }
 
-    private get extraTiles(): TemporaryTile[] | null {
+    private get extraTiles(): ReactComponentElement<typeof ExtraTile>[] | null {
         if (this.state.filteredExtraTiles) {
             return this.state.filteredExtraTiles;
         }
-        if (this.props.extraBadTilesThatShouldntExist) {
-            return this.props.extraBadTilesThatShouldntExist;
+        if (this.props.extraTiles) {
+            return this.props.extraTiles;
         }
         return null;
     }
@@ -177,7 +175,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
     }
 
     public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
-        const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraBadTilesThatShouldntExist;
+        const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraTiles;
         // as the rooms can come in one by one we need to reevaluate
         // the amount of available rooms to cap the amount of requested visible rooms by the layout
         if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) {
@@ -200,8 +198,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
 
         // If we're supposed to handle extra tiles, take the performance hit and re-render all the
         // time so we don't have to consider them as part of the visible room optimization.
-        const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || [];
-        const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraBadTilesThatShouldntExist) || [];
+        const prevExtraTiles = this.props.extraTiles || [];
+        const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraTiles) || [];
         if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) {
             return true;
         }
@@ -249,10 +247,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
     private onListsUpdated = () => {
         const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer
 
-        if (this.props.extraBadTilesThatShouldntExist) {
+        if (this.props.extraTiles) {
             const nameCondition = RoomListStore.instance.getFirstNameFilterCondition();
             if (nameCondition) {
-                stateUpdates.filteredExtraTiles = this.props.extraBadTilesThatShouldntExist
+                stateUpdates.filteredExtraTiles = this.props.extraTiles
                     .filter(t => nameCondition.matches(t.props.displayName || ""));
             } else if (this.state.filteredExtraTiles) {
                 stateUpdates.filteredExtraTiles = null;

From 0936ea7e640ac10449150eaed1615bc99c52e70c Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Tue, 9 Mar 2021 14:46:37 +0200
Subject: [PATCH 346/389] feat: show edit button only when user has permissions

call appropriate functions for state events and edit message events
---
 src/components/structures/ViewSource.js | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index cfe28e9f73..39666edd65 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -24,6 +24,7 @@ import * as sdk from "../../index";
 import MatrixClientContext from "../../contexts/MatrixClientContext";
 import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
 import { canEditContent } from "../../utils/EventUtils";
+import { MatrixClientPeg } from '../../MatrixClientPeg';
 
 export default class ViewSource extends React.Component {
     static propTypes = {
@@ -156,6 +157,12 @@ export default class ViewSource extends React.Component {
         }
     }
 
+    canSendStateEvent(mxEvent) {
+        const cli = MatrixClientPeg.get();
+        const room = cli.getRoom(mxEvent.getRoomId());
+        return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
+    }
+
     render() {
         const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
         const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
@@ -163,7 +170,7 @@ export default class ViewSource extends React.Component {
         const isEditing = this.state.isEditing;
         const roomId = mxEvent.getRoomId();
         const eventId = mxEvent.getId();
-        const canEdit = canEditContent(this.props.mxEvent) || mxEvent.isState();
+        const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent);
         return (
             <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
                 <div>

From 41c87c757028ad00da4b4850374843d1661da00a Mon Sep 17 00:00:00 2001
From: Matthew Hodgson <matthew@matrix.org>
Date: Tue, 9 Mar 2021 13:35:42 +0000
Subject: [PATCH 347/389] remove obsolete comment

---
 src/stores/SetupEncryptionStore.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js
index 28ab76edc0..3839f27a77 100644
--- a/src/stores/SetupEncryptionStore.js
+++ b/src/stores/SetupEncryptionStore.js
@@ -97,7 +97,6 @@ export class SetupEncryptionStore extends EventEmitter {
 
         if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
             // skip before we can even render anything.
-            // XXX: this causes a dialog box flash
             this.phase = PHASE_FINISHED;
         } else {
             this.phase = PHASE_INTRO;

From dd0b0834e68e7f96c683604d23c06b94f0575f6c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 9 Mar 2021 13:40:48 +0000
Subject: [PATCH 348/389] Fix React warning

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

diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js
index 6dc9fc01b0..8207d334d3 100644
--- a/src/components/views/dialogs/InfoDialog.js
+++ b/src/components/views/dialogs/InfoDialog.js
@@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component {
         className: PropTypes.string,
         title: PropTypes.string,
         description: PropTypes.node,
-        button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool),
+        button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
         onFinished: PropTypes.func,
         hasCloseButton: PropTypes.bool,
         onKeyDown: PropTypes.func,

From cf2c79069983a525d878e3fc634ead908091a709 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 9 Mar 2021 13:41:06 +0000
Subject: [PATCH 349/389] Pass OOB data for suggested rooms

---
 src/components/views/rooms/RoomList.tsx | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 7b44647be6..1573945a17 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -440,6 +440,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
                 defaultDispatcher.dispatch({
                     action: "view_room",
                     room_id: room.room_id,
+                    oobData: {
+                        avatarUrl: room.avatar_url,
+                        name,
+                    },
                 });
             };
             return (

From 1cfeb3692716a8ea17a91fc14f1ef7cf756f0454 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 9 Mar 2021 13:41:37 +0000
Subject: [PATCH 350/389] Update suggested room on join

---
 src/stores/SpaceStore.tsx | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index 1ada5d6361..d1abc68f4e 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -355,6 +355,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
             // this.onRoomUpdate(room);
             this.onRoomsUpdate();
         }
+
+        const numSuggestedRooms = this._suggestedRooms.length;
+        this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
+        if (numSuggestedRooms !== this._suggestedRooms.length) {
+            this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
+        }
     };
 
     private onRoomState = (ev: MatrixEvent) => {

From e6370a970bc755f5c89ccd5ffc39ea0162800453 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 9 Mar 2021 14:03:58 +0000
Subject: [PATCH 351/389] Tweak call handler stuff to not explode the room list
 on unsupported servers

---
 src/VoipUserMapper.ts                 | 8 +++++---
 src/stores/room-list/RoomListStore.ts | 2 +-
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts
index d919615349..4f5613b4a8 100644
--- a/src/VoipUserMapper.ts
+++ b/src/VoipUserMapper.ts
@@ -37,7 +37,7 @@ export default class VoipUserMapper {
         return results[0].userid;
     }
 
-    public async getOrCreateVirtualRoomForRoom(roomId: string):Promise<string> {
+    public async getOrCreateVirtualRoomForRoom(roomId: string): Promise<string> {
         const userId = DMRoomMap.shared().getUserIdForRoomId(roomId);
         if (!userId) return null;
 
@@ -52,7 +52,7 @@ export default class VoipUserMapper {
         return virtualRoomId;
     }
 
-    public nativeRoomForVirtualRoom(roomId: string):string {
+    public nativeRoomForVirtualRoom(roomId: string): string {
         const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
         if (!virtualRoom) return null;
         const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
@@ -60,7 +60,7 @@ export default class VoipUserMapper {
         return virtualRoomEvent.getContent()['native_room'] || null;
     }
 
-    public isVirtualRoom(room: Room):boolean {
+    public isVirtualRoom(room: Room): boolean {
         if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
 
         if (this.virtualRoomIdCache.has(room.roomId)) return true;
@@ -79,6 +79,8 @@ export default class VoipUserMapper {
     }
 
     public async onNewInvitedRoom(invitedRoom: Room) {
+        if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
+
         const inviterId = invitedRoom.getDMInviter();
         console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
         const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts
index 60a960261c..5775e685fd 100644
--- a/src/stores/room-list/RoomListStore.ts
+++ b/src/stores/room-list/RoomListStore.ts
@@ -409,7 +409,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
     }
 
     private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
-        if (cause === RoomUpdateCause.NewRoom) {
+        if (cause === RoomUpdateCause.NewRoom && room.getMyMembership() === "invite") {
             // Let the visibility provider know that there is a new invited room. It would be nice
             // if this could just be an event that things listen for but the point of this is that
             // we delay doing anything about this room until the VoipUserMapper had had a chance

From 3ac4cb49fc7d2dfaac3c7c775c45f7fe6402d25f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 9 Mar 2021 14:16:29 +0000
Subject: [PATCH 352/389] Tweak styling of manage rooms in space ux

---
 res/css/structures/_SpaceRoomDirectory.scss | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss
index 5cb91820cf..c96398594f 100644
--- a/res/css/structures/_SpaceRoomDirectory.scss
+++ b/res/css/structures/_SpaceRoomDirectory.scss
@@ -203,8 +203,9 @@ limitations under the License.
         .mx_SpaceRoomDirectory_actions {
             width: 180px;
             text-align: right;
-            height: min-content;
-            margin: auto 0 auto 28px;
+            margin-left: 28px;
+            display: inline-flex;
+            align-items: center;
 
             .mx_AccessibleButton {
                 vertical-align: middle;
@@ -223,9 +224,5 @@ limitations under the License.
             line-height: $font-15px;
             color: $secondary-fg-color;
         }
-
-        .mx_Checkbox {
-            display: inline-block;
-        }
     }
 }

From 71b8959d3c12fca4d5cf88db5d2de215b22bb34a Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 9 Mar 2021 09:36:17 -0700
Subject: [PATCH 353/389] Apply suggestions from code review

Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
---
 src/components/views/rooms/BasicMessageComposer.tsx | 2 +-
 src/components/views/rooms/ForwardMessage.js        | 2 +-
 src/components/views/voip/DialPad.tsx               | 2 +-
 src/components/views/voip/DialPadModal.tsx          | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index 829809ad49..5ab2b82a32 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -106,7 +106,7 @@ interface IState {
     completionIndex?: number;
 }
 
-@replaceableComponent("views.rooms.BasixMessageEditor")
+@replaceableComponent("views.rooms.BasicMessageEditor")
 export default class BasicMessageEditor extends React.Component<IProps, IState> {
     private editorRef = createRef<HTMLDivElement>();
     private autocompleteRef = createRef<Autocomplete>();
diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js
index 222895ef04..dd894c0dcf 100644
--- a/src/components/views/rooms/ForwardMessage.js
+++ b/src/components/views/rooms/ForwardMessage.js
@@ -21,7 +21,7 @@ import { _t } from '../../../languageHandler';
 import {Key} from '../../../Keyboard';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 
-@replaceableComponent("views.rooms.FowardMessage")
+@replaceableComponent("views.rooms.ForwardMessage")
 export default class ForwardMessage extends React.Component {
     static propTypes = {
         onCancelClick: PropTypes.func.isRequired,
diff --git a/src/components/views/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx
index 3598f511f5..68092fb0be 100644
--- a/src/components/views/voip/DialPad.tsx
+++ b/src/components/views/voip/DialPad.tsx
@@ -60,7 +60,7 @@ interface IProps {
     onDialPress?: (string) => void;
 }
 
-@replaceableComponent("views.voip.Dialpad")
+@replaceableComponent("views.voip.DialPad")
 export default class Dialpad extends React.PureComponent<IProps> {
     render() {
         const buttonNodes = [];
diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx
index c085809d5a..cdd5bc6641 100644
--- a/src/components/views/voip/DialPadModal.tsx
+++ b/src/components/views/voip/DialPadModal.tsx
@@ -35,7 +35,7 @@ interface IState {
     value: string;
 }
 
-@replaceableComponent("views.voip.DialpadModal")
+@replaceableComponent("views.voip.DialPadModal")
 export default class DialpadModal extends React.PureComponent<IProps, IState> {
     constructor(props) {
         super(props);

From 375ffafda647a47f9390b272f93b84ca2d8571a0 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 9 Mar 2021 10:13:16 -0700
Subject: [PATCH 354/389] Fix tests failing to load skin

We should find a better way to do this, but this works well enough.
---
 test/accessibility/RovingTabIndex-test.js                        | 1 +
 .../components/views/messages/MKeyVerificationConclusion-test.js | 1 +
 test/components/views/rooms/SendMessageComposer-test.js          | 1 +
 test/createRoom-test.js                                          | 1 +
 test/editor/deserialize-test.js                                  | 1 +
 5 files changed, 5 insertions(+)

diff --git a/test/accessibility/RovingTabIndex-test.js b/test/accessibility/RovingTabIndex-test.js
index 8be4a2976c..5aa93f99f3 100644
--- a/test/accessibility/RovingTabIndex-test.js
+++ b/test/accessibility/RovingTabIndex-test.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import '../skinned-sdk'; // Must be first for skinning to work
 import React from "react";
 import Adapter from "enzyme-adapter-react-16";
 import { configure, mount } from "enzyme";
diff --git a/test/components/views/messages/MKeyVerificationConclusion-test.js b/test/components/views/messages/MKeyVerificationConclusion-test.js
index 689151fe3f..45e122295b 100644
--- a/test/components/views/messages/MKeyVerificationConclusion-test.js
+++ b/test/components/views/messages/MKeyVerificationConclusion-test.js
@@ -1,3 +1,4 @@
+import '../../../skinned-sdk'; // Must be first for skinning to work
 import React from 'react';
 import TestRenderer from 'react-test-renderer';
 import { EventEmitter } from 'events';
diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js
index 6eeac7ceea..64a90eee81 100644
--- a/test/components/views/rooms/SendMessageComposer-test.js
+++ b/test/components/views/rooms/SendMessageComposer-test.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import '../../../skinned-sdk'; // Must be first for skinning to work
 import Adapter from "enzyme-adapter-react-16";
 import { configure, mount } from "enzyme";
 import React from "react";
diff --git a/test/createRoom-test.js b/test/createRoom-test.js
index f7e8617c3f..ed8f9779f7 100644
--- a/test/createRoom-test.js
+++ b/test/createRoom-test.js
@@ -1,3 +1,4 @@
+import './skinned-sdk'; // Must be first for skinning to work
 import {_waitForMember, canEncryptToAllUsers} from '../src/createRoom';
 import {EventEmitter} from 'events';
 
diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js
index 112ac7d02b..07b75aaae5 100644
--- a/test/editor/deserialize-test.js
+++ b/test/editor/deserialize-test.js
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import '../skinned-sdk'; // Must be first for skinning to work
 import {parseEvent} from "../../src/editor/deserialize";
 import {createPartCreator} from "./mock";
 

From ac1ce24b74c631346a653a17e79ff20ddbc9e754 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 9 Mar 2021 20:52:30 -0700
Subject: [PATCH 355/389] Fix sent markers disappearing for edits/reactions

Fixes https://github.com/vector-im/element-web/issues/16651
---
 src/components/structures/MessagePanel.js | 24 +++++++++++++++++++----
 1 file changed, 20 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 9deda54bee..b0705b5878 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -534,10 +534,17 @@ export default class MessagePanel extends React.Component {
                     const nextEvent = i < this.props.events.length - 1
                         ? this.props.events[i + 1]
                         : null;
+
+                    // The next event with tile is used to to determine the 'last successful' flag
+                    // when rendering the tile. The shouldShowEvent function is pretty quick at what
+                    // it does, so this should have no significant cost even when a room is used for
+                    // not-chat purposes.
+                    const nextTile = this.props.events.slice(i + 1).find(e => this._shouldShowEvent(e));
+
                     // make sure we unpack the array returned by _getTilesForEvent,
                     // otherwise react will auto-generate keys and we will end up
                     // replacing all of the DOM elements every time we paginate.
-                    ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent));
+                    ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile));
                     prevEvent = mxEv;
                 }
 
@@ -553,7 +560,7 @@ export default class MessagePanel extends React.Component {
         return ret;
     }
 
-    _getTilesForEvent(prevEvent, mxEv, last, nextEvent) {
+    _getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
         const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
         const EventTile = sdk.getComponent('rooms.EventTile');
         const DateSeparator = sdk.getComponent('messages.DateSeparator');
@@ -598,12 +605,21 @@ export default class MessagePanel extends React.Component {
         let isLastSuccessful = false;
         const isSentState = s => !s || s === 'sent';
         const isSent = isSentState(mxEv.getAssociatedStatus());
-        if (!nextEvent && isSent) {
+        const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent);
+        if (!hasNextEvent && isSent) {
             isLastSuccessful = true;
-        } else if (nextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
+        } else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
             isLastSuccessful = true;
         }
 
+        // This is a bit nuanced, but if our next event is hidden but a future event is not
+        // hidden then we're not the last successful.
+        if (nextEventWithTile) { // avoid length limit by wrapping in an if
+            if (isSentState(nextEventWithTile.getAssociatedStatus()) && nextEventWithTile !== nextEvent) {
+                isLastSuccessful = false;
+            }
+        }
+
         // We only want to consider "last successful" if the event is sent by us, otherwise of course
         // it's successful: we received it.
         isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();

From f25db38b2b304b60c1c8b2c2d93eeaca9fb90742 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 9 Mar 2021 21:13:23 -0700
Subject: [PATCH 356/389] Add tooltips to sent/sending receipts

Fixes https://github.com/vector-im/element-web/issues/16649
---
 src/components/views/rooms/EventTile.js | 62 ++++++++++++++++++++++---
 src/i18n/strings/en_EN.json             |  3 ++
 2 files changed, 59 insertions(+), 6 deletions(-)

diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index a705e92d9c..ecc7eb7131 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -39,6 +39,7 @@ import {WidgetType} from "../../../widgets/WidgetType";
 import RoomAvatar from "../avatars/RoomAvatar";
 import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
 import {objectHasDiff} from "../../../utils/objects";
+import Tooltip from "../elements/Tooltip";
 
 const eventTileTypes = {
     'm.room.message': 'messages.MessageEvent',
@@ -567,11 +568,8 @@ export default class EventTile extends React.Component {
     };
 
     getReadAvatars() {
-        if (this._shouldShowSentReceipt) {
-            return <span className="mx_EventTile_readAvatars"><span className='mx_EventTile_receiptSent' /></span>;
-        }
-        if (this._shouldShowSendingReceipt) {
-            return <span className="mx_EventTile_readAvatars"><span className='mx_EventTile_receiptSending' /></span>;
+        if (this._shouldShowSentReceipt || this._shouldShowSendingReceipt) {
+            return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
         }
 
         // return early if there are no read receipts
@@ -1180,7 +1178,6 @@ class E2ePadlock extends React.Component {
     render() {
         let tooltip = null;
         if (this.state.hover) {
-            const Tooltip = sdk.getComponent("elements.Tooltip");
             tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} dir="auto" />;
         }
 
@@ -1195,3 +1192,56 @@ class E2ePadlock extends React.Component {
         );
     }
 }
+
+interface ISentReceiptProps {
+    messageState: string; // TODO: Types for message sending state
+}
+
+interface ISentReceiptState {
+    hover: boolean;
+}
+
+class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptState> {
+    constructor() {
+        super();
+
+        this.state = {
+            hover: false,
+        };
+    }
+
+    onHoverStart = () => {
+        this.setState({hover: true});
+    };
+
+    onHoverEnd = () => {
+        this.setState({hover: false});
+    };
+
+    render() {
+        const isSent = !this.props.messageState || this.props.messageState === 'sent';
+        const receiptClasses = classNames({
+            'mx_EventTile_receiptSent': isSent,
+            'mx_EventTile_receiptSending': !isSent,
+        });
+
+        let tooltip = null;
+        if (this.state.hover) {
+            let label = _t("Sending your message...");
+            if (this.props.messageState === 'encrypting') {
+                label = _t("Encrypting your message...");
+            } else if (isSent) {
+                label = _t("Your message was sent");
+            }
+            // The yOffset is somewhat arbitrary - it just brings the tooltip down to be more associated
+            // with the read receipt.
+            tooltip = <Tooltip className="mx_EventTile_readAvatars_receiptTooltip" label={label} yOffset={20} />;
+        }
+
+        return <span className="mx_EventTile_readAvatars">
+            <span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
+                {tooltip}
+            </span>
+        </span>;
+    }
+}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 833a8c7838..9ed75bde37 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1440,6 +1440,9 @@
     "Unencrypted": "Unencrypted",
     "Encrypted by a deleted session": "Encrypted by a deleted session",
     "The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.",
+    "Sending your message...": "Sending your message...",
+    "Encrypting your message...": "Encrypting your message...",
+    "Your message was sent": "Your message was sent",
     "Please select the destination room for this message": "Please select the destination room for this message",
     "Scroll to most recent messages": "Scroll to most recent messages",
     "Close preview": "Close preview",

From 78568d6a015c764ae1a5eaed70b35b0ab14d40e8 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 10 Mar 2021 01:40:43 -0700
Subject: [PATCH 357/389] Document behaviour of showReadReceipts=false for sent
 receipts

---
 src/components/structures/MessagePanel.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 9deda54bee..353fd80528 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -498,6 +498,9 @@ export default class MessagePanel extends React.Component {
 
         let prevEvent = null; // the last event we showed
 
+        // Note: the EventTile might still render a "sent/sending receipt" independent of
+        // this information. When not providing read receipt information, the tile is likely
+        // to assume that sent receipts are to be shown more often.
         this._readReceiptsByEvent = {};
         if (this.props.showReadReceipts) {
             this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();

From 0583ea6a253074f300f7e17c213762f974b8a47b Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 10 Mar 2021 12:11:48 +0000
Subject: [PATCH 358/389] Ignore to-device decryption in the room list store

This avoids meaningless warnings about "unknown" rooms.
---
 src/stores/room-list/RoomListStore.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts
index 5775e685fd..3f415f946d 100644
--- a/src/stores/room-list/RoomListStore.ts
+++ b/src/stores/room-list/RoomListStore.ts
@@ -302,6 +302,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
         } else if (payload.action === 'MatrixActions.Event.decrypted') {
             const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
             const roomId = eventPayload.event.getRoomId();
+            if (!roomId) {
+                return;
+            }
             const room = this.matrixClient.getRoom(roomId);
             if (!room) {
                 console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`);

From 32f737e1ba6e442974067c058dd192785e526d00 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 10 Mar 2021 13:45:37 +0000
Subject: [PATCH 359/389] Tweak sent marker code style

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

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index b0705b5878..f8bf554cb3 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -614,10 +614,12 @@ export default class MessagePanel extends React.Component {
 
         // This is a bit nuanced, but if our next event is hidden but a future event is not
         // hidden then we're not the last successful.
-        if (nextEventWithTile) { // avoid length limit by wrapping in an if
-            if (isSentState(nextEventWithTile.getAssociatedStatus()) && nextEventWithTile !== nextEvent) {
-                isLastSuccessful = false;
-            }
+        if (
+            nextEventWithTile &&
+            nextEventWithTile !== nextEvent &&
+            isSentState(nextEventWithTile.getAssociatedStatus())
+        ) {
+            isLastSuccessful = false;
         }
 
         // We only want to consider "last successful" if the event is sent by us, otherwise of course

From fc180cd6d4efc02c2193efbbc23c628fdf3e1d05 Mon Sep 17 00:00:00 2001
From: "@a2sc:matrix.org" <a0_r@a2sc.eu>
Date: Mon, 8 Mar 2021 23:12:51 +0000
Subject: [PATCH 360/389] Translated using Weblate (German)

Currently translated at 99.6% (2769 of 2780 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 | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 7304d35fcd..f0f371ebe5 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -3076,5 +3076,6 @@
     "Settable at global": "Global festlegbar",
     "Setting definition:": "Definition der Einstellung:",
     "Value in this room": "Wert in diesem Raum",
-    "Settings Explorer": "Einstellungs-Explorer"
+    "Settings Explorer": "Einstellungs-Explorer",
+    "Values at explicit levels": "Werte für explizite Levels"
 }

From 73327c9bc68b166e6af1973c786aa067524f2421 Mon Sep 17 00:00:00 2001
From: libexus <Asterixeins324@gmail.com>
Date: Mon, 8 Mar 2021 23:12:26 +0000
Subject: [PATCH 361/389] Translated using Weblate (German)

Currently translated at 99.6% (2769 of 2780 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 | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index f0f371ebe5..65e08ae4c3 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -3073,9 +3073,10 @@
     "Show chat effects (animations when receiving e.g. confetti)": "Animierte Chateffekte zeigen, wenn z.B. Konfetti-Emojis erhalten werden",
     "Save setting values": "Einstellungswerte speichern",
     "Caution:": "Vorsicht:",
-    "Settable at global": "Global festlegbar",
+    "Settable at global": "Global einstellbar",
     "Setting definition:": "Definition der Einstellung:",
     "Value in this room": "Wert in diesem Raum",
     "Settings Explorer": "Einstellungs-Explorer",
-    "Values at explicit levels": "Werte für explizite Levels"
+    "Values at explicit levels": "Werte für explizite Levels",
+    "Settable at room": "Für den Raum einstellbar"
 }

From 2125438aac407222b88d4578bf2ce187c6301999 Mon Sep 17 00:00:00 2001
From: Tirifto <tirifto@posteo.cz>
Date: Wed, 10 Mar 2021 09:27:48 +0000
Subject: [PATCH 362/389] Translated using Weblate (Esperanto)

Currently translated at 99.1% (2755 of 2780 strings)

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

diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json
index 3f99a68187..390fb74099 100644
--- a/src/i18n/strings/eo.json
+++ b/src/i18n/strings/eo.json
@@ -3006,5 +3006,44 @@
     "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Via hejmservilo estis neatingebla kaj ne povis vin salutigi. Bonvolu reprovi. Se tio daŭros, bonvolu kontakti la administranton de via hejmservilo.",
     "Try again": "Reprovu",
     "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Ni petis la foliumilon memori, kiun hejmservilon vi uzas por saluti, sed domaĝe, via foliumilo forgesis. Iru al la saluta paĝo kaj reprovu.",
-    "We couldn't log you in": "Ni ne povis salutigi vin"
+    "We couldn't log you in": "Ni ne povis salutigi vin",
+    "%(creator)s created this DM.": "%(creator)s kreis ĉi tiun rektan ĉambron.",
+    "Invalid URL": "Nevalida URL",
+    "Unable to validate homeserver": "Ne povas validigi hejmservilon",
+    "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>.": "Averte, se vi ne aldonos retpoŝtadreson kaj poste forgesos vian pasvorton, vi eble <b>por ĉiam perdos aliron al via konto</b>.",
+    "Continuing without email": "Daŭrigante sen retpoŝtadreso",
+    "We recommend you change your password and Security Key in Settings immediately": "Ni rekomendas, ke vi tuj ŝanĝu viajn pasvorton kaj Sekurecan ŝlosilon per la Agordoj",
+    "Transfer": "Transigi",
+    "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invitu iun per ĝia nomo, retpoŝtadreso, uzantonomo (ekz. <userId/>), aŭ <a>konigu ĉi tiun ĉambron</a>.",
+    "Start a conversation with someone using their name, email address or username (like <userId/>).": "Komencu interparolon kun iu per ĝia nomo, retpoŝtadreso, aŭ uzantonomo (ekz. <userId/>).",
+    "Failed to transfer call": "Malsukcesis transigi vokon",
+    "A call can only be transferred to a single user.": "Voko povas transiĝi nur al unu uzanto.",
+    "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Eksciu plion per niaj <privacyPolicyLink />, <termsOfServiceLink /> kaj <cookiePolicyLink />.",
+    "Failed to connect to your homeserver. Please close this dialog and try again.": "Malsukcesis konektiĝi al via hejmservilo. Bonvolu fermi ĉi tiun interagujon kaj reprovi.",
+    "Edit Values": "Redakti valorojn",
+    "Value in this room:": "Valoro en ĉi tiu ĉambro:",
+    "Value:": "Valoro:",
+    "Settable at room": "Agordebla ĉambre",
+    "Settable at global": "Agordebla ĉiee",
+    "Level": "Nivelo",
+    "Setting definition:": "Difino de agordo:",
+    "This UI does NOT check the types of the values. Use at your own risk.": "Ĉi tiu fasado ne kontrolas la tipojn de valoroj. Uzu je via risko.",
+    "Caution:": "Atentu:",
+    "Setting:": "Agordo:",
+    "Value in this room": "Valoro en ĉi tiu ĉambro",
+    "Value": "Valoro",
+    "Setting ID": "Identigilo de agordo",
+    "Failed to save settings": "Malsukcesis konservi agordojn",
+    "Settings Explorer": "Esplorilo de agordoj",
+    "Set my room layout for everyone": "Agordi al ĉiuj mian aranĝon de ĉambro",
+    "Open dial pad": "Malfermi ciferplaton",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Savkopiu viajn čifrajn šlosilojn kune kun la datumoj de via konto, okaze ke vi perdos aliron al viaj salutaĵoj. Viaj ŝlosiloj sekuriĝos per unika Sekureca ŝlosilo.",
+    "Dial pad": "Ciferplato",
+    "Show chat effects (animations when receiving e.g. confetti)": "Montri grafikaĵojn en babilujo (ekz. movbildojn, ricevante konfetojn)",
+    "Use Ctrl + Enter to send a message": "Sendu mesaĝon per stirklavo (Ctrl) + eniga klavo",
+    "Use Command + Enter to send a message": "Sendu mesaĝon per komanda klavo + eniga klavo",
+    "Use Ctrl + F to search": "Serĉu per stirklavo (Ctrl) + F",
+    "Use Command + F to search": "Serĉu per komanda klavo + F",
+    "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s ŝanĝis la servilblokajn listojn por ĉi tiu ĉambro.",
+    "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s agordis la servilblokajn listojn por ĉi tiu ĉambro."
 }

From 7c497cc96e2e6d566008e15e44ccc50d5319b210 Mon Sep 17 00:00:00 2001
From: jelv <post@jelv.nl>
Date: Wed, 10 Mar 2021 15:13:56 +0000
Subject: [PATCH 363/389] Translated using Weblate (Dutch)

Currently translated at 100.0% (2780 of 2780 strings)

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

diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index db3255c051..18c7c3b0bf 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -145,7 +145,7 @@
     "Displays action": "Toont actie",
     "Emoji": "Emoji",
     "%(senderName)s ended the call.": "%(senderName)s heeft opgehangen.",
-    "Enter passphrase": "Voer wachtwoord in",
+    "Enter passphrase": "Wachtwoord invoeren",
     "Error decrypting attachment": "Fout bij het ontsleutelen van de bijlage",
     "Error: Problem communicating with the given homeserver.": "Fout: probleem bij communicatie met de gegeven thuisserver.",
     "Existing Call": "Bestaande oproep",
@@ -335,11 +335,11 @@
     "Passphrases must match": "Wachtwoorden moeten overeenkomen",
     "Passphrase must not be empty": "Wachtwoord mag niet leeg zijn",
     "Export room keys": "Gesprekssleutels wegschrijven",
-    "Confirm passphrase": "Bevestig wachtwoord",
+    "Confirm passphrase": "Wachtwoord bevestigen",
     "Import room keys": "Gesprekssleutels inlezen",
     "File to import": "In te lezen bestand",
     "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Hiermee kunt u de sleutels van uw ontvangen berichten in versleutelde gesprekken naar een lokaal bestand wegschrijven. Als u dat bestand dan in een andere Matrix-cliënt inleest kan die ook die berichten ontcijferen.",
-    "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.": "Wie het weggeschreven bestand kan lezen, kan daarmee ook alle versleutelde berichten die u kunt zien ontcijferen - ga er dus zorgvuldig mee om! Daartoe kunt u hieronder een wachtwoord invoeren, dat dan gebruikt zal worden om het bestand te versleutelen. Het is dan enkel mogelijk de gegevens in te lezen met hetzelfde wachtwoord.",
+    "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.": "Het opgeslagen bestand geeft toegang tot het lezen en schrijven van uw versleutelde berichten - ga er dus zorgvuldig mee om! Bescherm uzelf door hieronder een wachtwoord in te voeren, dat dan gebruikt zal worden om het bestand te versleutelen. Het is dan alleen mogelijk de gegevens te lezen met hetzelfde wachtwoord.",
     "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Hiermee kunt u vanuit een andere Matrix-cliënt weggeschreven versleutelingssleutels inlezen, zodat u alle berichten die de andere cliënt kon ontcijferen ook hier kunt lezen.",
     "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Het weggeschreven bestand is beveiligd met een wachtwoord. Voer dat wachtwoord hier in om het bestand te ontsleutelen.",
     "You must join the room to see its files": "Slechts na toetreding tot het gesprek zult u de bestanden kunnen zien",
@@ -792,7 +792,7 @@
     "Enable widget screenshots on supported widgets": "Widget-schermafbeeldingen inschakelen op ondersteunde widgets",
     "Muted Users": "Gedempte gebruikers",
     "Popout widget": "Widget in nieuw venster openen",
-    "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Kan de gebeurtenis waarop gereageerd was niet laden. Wellicht bestaat die niet, of heeft u geen toestemming die te bekijken.",
+    "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Kan de gebeurtenis waarop gereageerd was niet laden. Wellicht bestaat die niet, of u heeft geen toestemming die te bekijken.",
     "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Dit zal uw account voorgoed onbruikbaar maken. U zult niet meer kunnen inloggen, en niemand anders zal zich met dezelfde gebruikers-ID kunnen registreren. Hierdoor zal uw account alle gesprekken waaraan u deelneemt verlaten, en worden de accountgegevens verwijderd van de identiteitsserver. <b>Deze stap is onomkeerbaar.</b>",
     "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Het sluiten van uw account <b>maakt op zich niet dat wij de door u verstuurde berichten vergeten.</b> Als u wilt dat wij uw berichten vergeten, vink dan het vakje hieronder aan.",
     "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "De zichtbaarheid van berichten in Matrix is zoals bij e-mails. Het vergeten van uw berichten betekent dat berichten die u heeft verstuurd niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan.",
@@ -1641,7 +1641,7 @@
     "Your user agent": "Jouw gebruikersagent",
     "If you cancel now, you won't complete verifying the other user.": "Als u nu annuleert zult u de andere gebruiker niet verifiëren.",
     "If you cancel now, you won't complete verifying your other session.": "Als u nu annuleert zult u uw andere sessie niet verifiëren.",
-    "Cancel entering passphrase?": "Wachtwoordinvoer annuleren?",
+    "Cancel entering passphrase?": "Wachtwoord annuleren?",
     "Show typing notifications": "Typmeldingen weergeven",
     "Verify this session by completing one of the following:": "Verifieer deze sessie door een van het volgende te doen:",
     "Scan this unique code": "Scan deze unieke code",
@@ -1981,8 +1981,8 @@
     "Confirm adding phone number": "Bevestig toevoegen van het telefoonnummer",
     "Click the button below to confirm adding this phone number.": "Klik op de knop hieronder om het toevoegen van dit telefoonnummer te bevestigen.",
     "If you cancel now, you won't complete your operation.": "Als u de operatie afbreekt kunt u haar niet voltooien.",
-    "Review where you’re logged in": "Kijk na waar u ingelogd bent",
-    "New login. Was this you?": "Nieuwe login - was u dat?",
+    "Review where you’re logged in": "Controleer waar u ingelogd bent",
+    "New login. Was this you?": "Nieuwe login gevonden. Was u dat?",
     "%(name)s is requesting verification": "%(name)s verzoekt om verificatie",
     "Sends a message as html, without interpreting it as markdown": "Stuurt een bericht als HTML, zonder markdown toe te passen",
     "Failed to set topic": "Kon onderwerp niet instellen",
@@ -2330,8 +2330,8 @@
     "Start a conversation with someone using their name, email address or username (like <userId/>).": "Start een gesprek met iemand door hun naam, emailadres of gebruikersnaam (zoals <userId/>) te typen.",
     "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Berichten hier zijn eind-tot-eind versleuteld. Verifieer %(displayName)s op hun profiel - klik op hun avatar.",
     "%(creator)s created this DM.": "%(creator)s maakte deze DM.",
-    "Switch to dark mode": "Wissel naar donkere modus",
-    "Switch to light mode": "Wissel naar lichte modus",
+    "Switch to dark mode": "Naar donkere modus wisselen",
+    "Switch to light mode": "Naar lichte modus wisselen",
     "Appearance": "Weergave",
     "All settings": "Alle instellingen",
     "Error removing address": "Fout bij verwijderen van adres",
@@ -2696,7 +2696,7 @@
     "Calls": "Oproepen",
     "Navigation": "Navigatie",
     "Currently indexing: %(currentRoom)s": "Momenteel indexeren: %(currentRoom)s",
-    "Enter your recovery passphrase a second time to confirm it.": "Voer uw herstel wachtwoord een tweede keer in om te bevestigen.",
+    "Enter your recovery passphrase a second time to confirm it.": "Voer uw Herstelwachtwoord een tweede keer in om te bevestigen.",
     "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Deze sessie heeft ontdekt dat uw veiligheidszin en sleutel voor versleutelde berichten zijn verwijderd.",
     "A new Security Phrase and key for Secure Messages have been detected.": "Er is een nieuwe veiligheidszin en sleutel voor versleutelde berichten gedetecteerd.",
     "Save your Security Key": "Uw veiligheidssleutel opslaan",
@@ -2708,7 +2708,7 @@
     "Secret storage:": "Sleutelopslag:",
     "Unable to query secret storage status": "Kan status sleutelopslag niet opvragen",
     "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Bewaar uw veiligheidssleutel op een veilige plaats, zoals in een wachtwoordmanager of een kluis, aangezien deze wordt gebruikt om uw versleutelde gegevens te beveiligen.",
-    "Confirm your recovery passphrase": "Bevestig uw herstel wachtwoord",
+    "Confirm your recovery passphrase": "Bevestig uw Herstelwachtwoord",
     "Use a different passphrase?": "Gebruik een ander wachtwoord?",
     "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.": "Voer een veiligheidszin in die alleen u kent, aangezien deze wordt gebruikt om uw gegevens te versleutelen. Om veilig te zijn, moet u het wachtwoord van uw account niet opnieuw gebruiken.",
     "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Bescherm uw server tegen toegangsverlies tot versleutelde berichten en gegevens door een back-up te maken van de versleutelingssleutels.",
@@ -2797,7 +2797,7 @@
     "Not a valid Security Key": "Geen geldige veiligheidssleutel",
     "This looks like a valid Security Key!": "Dit lijkt op een geldige veiligheidssleutel!",
     "Enter Security Key": "Veiligheidssleutel invoeren",
-    "If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Als u uw veiligheidszin bent vergeten, kunt u <button1>uw veiligheidssleutel gebruiken</button1> of <button2>nieuwe herstelopties instellen</button2>",
+    "If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Als u uw Herstelwachtwoord bent vergeten, kunt u <button1>uw Herstelsleutel gebruiken</button1> of <button2>nieuwe herstelopties instellen</button2>",
     "Access your secure message history and set up secure messaging by entering your Security Phrase.": "Ga naar uw veilige berichtengeschiedenis en stel veilige berichten in door uw veiligheidszin in te voeren.",
     "Enter Security Phrase": "Voer veiligheidszin in",
     "Successfully restored %(sessionCount)s keys": "Succesvol %(sessionCount)s sleutels hersteld",

From e2cdc93be4a200f7d8c7bf759ad4f8413870d814 Mon Sep 17 00:00:00 2001
From: MamasLT <admin@eastwesthost.com>
Date: Tue, 9 Mar 2021 13:39:41 +0000
Subject: [PATCH 364/389] Translated using Weblate (Lithuanian)

Currently translated at 68.5% (1907 of 2780 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 | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json
index 80b70e67e4..1973247e66 100644
--- a/src/i18n/strings/lt.json
+++ b/src/i18n/strings/lt.json
@@ -2089,5 +2089,7 @@
     "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Jei jūsų kiti seansai neturi šios žinutės rakto, jūs negalėsite jos iššifruoti.",
     "Missing session data": "Trūksta seanso duomenų",
     "Successfully restored %(sessionCount)s keys": "Sėkmingai atkurti %(sessionCount)s raktai",
-    "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Įspėjimas: Jūsų asmeniniai duomenys (įskaitant šifravimo raktus) vis dar yra saugomi šiame seanse. Išvalykite juos, jei baigėte naudoti šį seansą, arba norite prisijungti prie kitos paskyros."
+    "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Įspėjimas: Jūsų asmeniniai duomenys (įskaitant šifravimo raktus) vis dar yra saugomi šiame seanse. Išvalykite juos, jei baigėte naudoti šį seansą, arba norite prisijungti prie kitos paskyros.",
+    "Reason (optional)": "Priežastis (nebūtina)",
+    "Reason: %(reason)s": "Priežastis: %(reason)s"
 }

From 0fe1e2dc182eb833a832fd242cd28b235bcb7072 Mon Sep 17 00:00:00 2001
From: tateisu <tateisu@gmail.com>
Date: Wed, 10 Mar 2021 13:37:09 +0000
Subject: [PATCH 365/389] Translated using Weblate (Japanese)

Currently translated at 79.1% (2199 of 2780 strings)

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

diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index ea7d51ae05..0035bdd5df 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -341,7 +341,7 @@
     "Unable to connect to Homeserver. Retrying...": "ホームサーバーに接続できません。 再試行中...",
     "Your browser does not support the required cryptography extensions": "お使いのブラウザは、必要な暗号化拡張機能をサポートしていません",
     "Not a valid %(brand)s keyfile": "有効な%(brand)sキーファイルではありません",
-    "Authentication check failed: incorrect password?": "認証に失敗しました: パスワードの間違っている可能性があります。",
+    "Authentication check failed: incorrect password?": "認証に失敗しました: 間違ったパスワード?",
     "Sorry, your homeserver is too old to participate in this room.": "申し訳ありませんが、あなたのホームサーバーはこの部屋に参加するには古すぎます。",
     "Please contact your homeserver administrator.": "ホームサーバー管理者に連絡してください。",
     "Failed to join room": "部屋に参加できませんでした",
@@ -554,7 +554,7 @@
     "Only visible to community members": "コミュニティメンバーにのみ表示されます",
     "Filter community rooms": "コミュニティルームを絞り込む",
     "Something went wrong when trying to get your communities.": "コミュニティに参加しようとすると何かがうまくいかなかった。",
-    "Display your community flair in rooms configured to show it.": "表示するよう設定した部屋であなたのコミュニティ バッジを表示",
+    "Display your community flair in rooms configured to show it.": "表示するよう設定した部屋であなたのコミュニティ バッジを表示します。",
     "Show developer tools": "開発者ツールを表示",
     "You're not currently a member of any communities.": "あなたは現在、どのコミュニティのメンバーでもありません。",
     "Unknown Address": "不明な住所",
@@ -875,7 +875,7 @@
     "Open Devtools": "開発ツールを開く",
     "Flair": "バッジ",
     "Fill screen": "フィルスクリーン",
-    "Unignore": "無視しない",
+    "Unignore": "無視をやめる",
     "Unable to load! Check your network connectivity and try again.": "ロードできません! ネットワーク通信を確認の上もう一度お試しください。",
     "Failed to invite users to the room:": "部屋にユーザーを招待できませんでした:",
     "You do not have permission to invite people to this room.": "この部屋にユーザーを招待する権限がありません。",
@@ -1064,7 +1064,7 @@
     "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s がこの部屋に %(groups)s のバッジを追加しました。",
     "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s がこの部屋から %(groups)s のバッジを削除しました。",
     "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s が %(newGroups)s のバッジを追加し、 %(oldGroups)s のバッジを削除しました。",
-    "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "キーが正常にバックアップされていない場合、暗号化されたメッセージにアクセスできなくなります。本当によろしいですか?",
+    "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "本当によろしいですか? もしキーが正常にバックアップされていない場合、暗号化されたメッセージにアクセスできなくなります。",
     "not stored": "保存されていません",
     "All keys backed up": "すべてのキーがバックアップされました",
     "Backup version: ": "バックアップのバージョン: ",
@@ -1072,7 +1072,7 @@
     "Backup key stored: ": "バックアップキーの保存: ",
     "Back up your keys before signing out to avoid losing them.": "暗号化キーを失くさないために、サインアウトする前にキーをバックアップしてください。",
     "Start using Key Backup": "キーのバックアップをはじめる",
-    "Error updating flair": "バッジの更新でエラーが発生しました。",
+    "Error updating flair": "バッジの更新でエラーが発生しました",
     "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "この部屋のバッジの更新でエラーが発生しました。サーバーが許可していないか、一時的なエラーが発生しました。",
     "Edited at %(date)s. Click to view edits.": "%(date)sに編集。クリックして編集を表示。",
     "edited": "編集済み",
@@ -1103,7 +1103,7 @@
     "Session ID:": "セッション ID:",
     "Session key:": "セッション鍵:",
     "Cross-signing": "クロス署名",
-    "A session's public name is visible to people you communicate with": "各セッションの公開名は、あなたの連絡先のユーザーが閲覧できます。",
+    "A session's public name is visible to people you communicate with": "各セッションの公開名は、あなたの連絡先のユーザーが閲覧できます",
     "Session name": "セッション名",
     "Session key": "セッション鍵",
     "Never send encrypted messages to unverified sessions from this session": "このセッションでは、未検証のセッションに対して暗号化されたメッセージを送信しない",
@@ -1161,7 +1161,7 @@
     "Confirm": "確認",
     "Enable audible notifications for this session": "このセッションでは音声通知を有効にする",
     "Enable encryption?": "暗号化を有効にしますか?",
-    "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "一度有効にした部屋の暗号化は無効にすることはできません。暗号化された部屋で送信されたメッセージは、サーバーからは見ることができず、その部屋の参加者だけが見ることができます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる場合があります。<a>暗号化についての詳細はこちらをご覧ください</a>。",
+    "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "一度有効にした部屋の暗号化は無効にすることはできません。暗号化された部屋で送信されたメッセージは、サーバーからは見ることができず、その部屋の参加者だけが見ることができます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる場合があります。<a>暗号化についての詳細はこちらをご覧ください。</a>",
     "Enter username": "ユーザー名を入力",
     "Email (optional)": "メールアドレス (任意)",
     "Phone (optional)": "電話番号 (任意)",
@@ -1208,7 +1208,7 @@
     "Suggestions": "提案",
     "Start a conversation with someone using their name, username (like <userId/>) or email address.": "相手の名前、( <userId/> のような)ユーザー名、メールアドレスを使って会話を開始できます。",
     "Go": "続行",
-    "Session already verified!": "このセッションは検証済みです。",
+    "Session already verified!": "このセッションは検証済みです!",
     "WARNING: Session already verified, but keys do NOT MATCH!": "警告: このセッションは検証済みです、しかし鍵が一致していません!",
     "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "警告: 鍵の検証に失敗しました!提供された鍵「%(fingerprint)s」は、%(userId)s およびセッション %(deviceId)s の署名鍵「%(fprint)s」と一致しません。これはつまり、あなたの会話が傍受・盗聴されようとしている恐れがあるということです!",
     "Show typing notifications": "入力中通知を表示する",
@@ -1234,11 +1234,11 @@
     " to store messages from ": " を使用中であり ",
     "rooms.": "件の部屋のメッセージが含まれています。",
     "Manage": "管理",
-    "Add an email address to configure email notifications": "メールアドレスを追加すると電子メール通知の設定も行えます。",
+    "Add an email address to configure email notifications": "メールアドレスを追加すると電子メール通知の設定も行えます",
     "Custom theme URL": "カスタムテーマ URL",
     "Add theme": "テーマの追加",
     "Account management": "アカウントの管理",
-    "Deactivating your account is a permanent action - be careful!": "アカウントの無効化は取り消せません。ご注意ください。",
+    "Deactivating your account is a permanent action - be careful!": "アカウントの無効化は取り消せません。注意してください!",
     "Deactivate account": "アカウントの無効化",
     "Room list": "部屋一覧",
     "Timeline": "タイムライン",
@@ -1283,7 +1283,7 @@
     "Backup has a signature from <verify>unknown</verify> session with ID %(deviceId)s": "バックアップには、ID %(deviceId)s の<verify>未知のセッション</verify>による署名があります",
     "Backup has a <validity>valid</validity> signature from this session": "バックアップには、このセッションによる<validity>有効</validity>な署名があります",
     "Backup has an <validity>invalid</validity> signature from this session": "バックアップには、このセッションによる<validity>無効</validity>な署名があります",
-    "Backup has a <validity>valid</validity> signature from <verify>verified</verify> session <device></device>": "バックアップには、<verify>検証済み</verify>のセッション <device></device> による<validity>有効</validity>な署名があります",
+    "Backup has a <validity>valid</validity> signature from <verify>verified</verify> session <device></device>": "バックアップには<verify>検証された</verify>セッションの<device></device> による<validity>有効な</validity>署名があります",
     "Backup has a <validity>valid</validity> signature from <verify>unverified</verify> session <device></device>": "バックアップには、<verify>未検証</verify>のセッション <device></device> による<validity>有効</validity>な署名があります",
     "Backup has an <validity>invalid</validity> signature from <verify>verified</verify> session <device></device>": "バックアップには、<verify>検証済み</verify>のセッション <device></device> による<validity>無効</validity>な署名があります",
     "Backup has an <validity>invalid</validity> signature from <verify>unverified</verify> session <device></device>": "バックアップには、<verify>未検証</verify>のセッション <device></device> による<validity>無効</validity>な署名があります",
@@ -1313,7 +1313,7 @@
     "Autocomplete delay (ms)": "自動補完の遅延 (ms)",
     "Missing media permissions, click the button below to request.": "メディア権限が不足しています、リクエストするには下のボタンを押してください。",
     "Request media permissions": "メディア権限をリクエスト",
-    "Joining room …": "部屋に参加中...",
+    "Joining room …": "部屋に参加中…",
     "Join the discussion": "話し合いに参加",
     "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s はプレビューできません。部屋に参加しますか?",
     "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "他のユーザーがあなたのホームサーバー (%(localDomain)s) を通じてこの部屋を見つけられるよう、アドレスを設定しましょう",
@@ -1436,9 +1436,9 @@
     "Report bugs & give feedback": "バグ報告とフィードバック",
     "Everyone in this room is verified": "この部屋内の全員を検証済み",
     "Verify all users in a room to ensure it's secure.": "この部屋内のすべてのユーザーが安全であることを確認しました。",
-    "You've successfully verified %(displayName)s!": "%(displayName)s は正常に検証されました。",
-    "You've successfully verified %(deviceName)s (%(deviceId)s)!": "%(deviceName)s (%(deviceId)s) は正常に検証されました。",
-    "You've successfully verified your device!": "このデバイスは正常に検証されました。",
+    "You've successfully verified %(displayName)s!": "%(displayName)s は正常に検証されました!",
+    "You've successfully verified %(deviceName)s (%(deviceId)s)!": "%(deviceName)s (%(deviceId)s) は正常に検証されました!",
+    "You've successfully verified your device!": "このデバイスは正常に検証されました!",
     "You've successfully verified this user.": "このユーザーは正常に検証されました。",
     "Reject & Ignore user": "拒否した上でこのユーザーを無視する",
     "<userName/> invited you": "<userName/> があなたを招待しています",
@@ -1452,7 +1452,7 @@
     "Got it": "了解",
     "Got It": "了解",
     "Accepting…": "了承中…",
-    "Waiting for %(displayName)s to verify…": "%(displayName)s が検証するのを待っています…",
+    "Waiting for %(displayName)s to verify…": "%(displayName)s による検証を待っています…",
     "Waiting for %(displayName)s to accept…": "%(displayName)s が了承するのを待っています…",
     "Room avatar": "部屋のアバター",
     "Start Verification": "検証を開始",
@@ -1484,7 +1484,7 @@
     "e.g. my-room": "例: my-room",
     "Room address": "ルームアドレス",
     "New published address (e.g. #alias:server)": "新しい公開アドレス (例: #alias:server)",
-    "No other published addresses yet, add one below": "現在、公開アドレスがありません。以下から追加可能です。",
+    "No other published addresses yet, add one below": "現在、公開アドレスがありません。以下から追加可能です",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "検索結果を表示させるために、暗号化されたメッセージをローカルに安全にキャッシュしています。現在、%(rooms)s 件の部屋のメッセージの保存に %(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": "検索結果を表示させるために、暗号化されたメッセージをローカルに安全にキャッシュしています。現在、%(rooms)s 件の部屋のメッセージの保存に %(size)s を使用中です。",
     "Mentions & Keywords": "メンションとキーワード",
@@ -1531,9 +1531,810 @@
     "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.": "鍵共有リクエストは自動的にあなたの他のセッションに送信されます。他のセッションで鍵共有リクエストを拒否または却下した場合は、ここをクリックしてこのセッションの鍵を再度リクエストしてください。",
     "Your key share request has been sent - please check your other sessions for key share requests.": "鍵共有リクエストが送信されました。あなたの他のセッションで鍵共有リクエストをご確認ください。",
     "<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "あなたの他のセッションに<requestLink>暗号鍵を再リクエストする</requestLink>。",
-    "Block anyone not part of %(serverName)s from ever joining this room.": "%(serverName)s 以外からの参加をブロック",
+    "Block anyone not part of %(serverName)s from ever joining this room.": "%(serverName)s 以外からの参加をブロックします。",
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "プライベートな部屋は招待者のみが参加できます。公開された部屋は誰でも検索・参加できます。",
     "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "Matrix 関連のセキュリティ問題を報告するには、Matrix.org の <a>Security Disclosure Policy</a> をご覧ください。",
     "Confirm adding email": "メールアドレスの追加を確認する",
-    "Confirm adding this email address by using Single Sign On to prove your identity.": "シングルサインオンを使用して本人確認を行い、メールアドレスの追加を承認してください。"
+    "Confirm adding this email address by using Single Sign On to prove your identity.": "シングルサインオンを使用して本人確認を行い、メールアドレスの追加を承認してください。",
+    "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "そのアドレスの作成中にエラーが発生しました。 サーバーで許可されていないか、一時的な障害が発生した可能性があります。",
+    "Error creating address": "アドレスの作成中にエラーが発生しました",
+    "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "部屋の代替アドレスの更新中にエラーが発生しました。 サーバーで許可されていないか、一時的な障害が発生した可能性があります。",
+    "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "部屋のメインアドレスの更新中にエラーが発生しました。 サーバーで許可されていないか、一時的な障害が発生した可能性があります。",
+    "Error updating main address": "メインアドレスの更新中にエラーが発生しました",
+    "Mark all as read": "すべて既読としてマーク",
+    "Invited by %(sender)s": "%(sender)s からの招待",
+    "Revoke invite": "招待を取り消す",
+    "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "招待を取り消すことができませんでした。 サーバーで一時的な問題が発生しているか、招待を取り消すための十分な権限がない可能性があります。",
+    "Failed to revoke invite": "招待を取り消せませんでした",
+    "Hint: Begin your message with <code>//</code> to start it with a slash.": "ヒント: 通常メッセージをスラッシュで開始したい場合は <code>//</code> から始めます。",
+    "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "<code>/help</code>を使って利用可能なコマンドを一覧できます。メッセージとして送るつもりでしたか?",
+    "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "この部屋はホームサーバが<i>不安定</i>と判断した部屋バージョン<roomVersion />で動作しています。",
+    "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "この部屋をアップグレードすると、部屋の現在のインスタンスがシャットダウンされて、同じ名前でアップグレードされた部屋が作成されます。",
+    "This room has already been upgraded.": "この部屋はすでにアップグレードされています。",
+    "Unread messages.": "未読メッセージ。",
+    "%(count)s unread messages.|one": "未読メッセージ1件。",
+    "%(count)s unread messages.|other": "未読メッセージ%(count)s件。",
+    "%(count)s unread messages including mentions.|one": "未読のメンション1件。",
+    "%(count)s unread messages including mentions.|other": "メンションを含む未読メッセージ%(count)s件。",
+    "Jump to first invite.": "最初の招待にジャンプします。",
+    "Jump to first unread room.": "未読のある最初の部屋にジャンプします。",
+    "A-Z": "A-Z",
+    "Activity": "活発さ",
+    "Show previews of messages": "メッセージのプレビューを表示する",
+    "Show rooms with unread messages first": "未読メッセージのある部屋を最初に表示する",
+    "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "部屋にアクセスしようとした際に エラー(%(errcode)s)が発生しました。このメッセージが誤って表示されていると思われる場合は<issueLink>バグレポートを送信してください</issueLink>。",
+    "Try again later, or ask a room admin to check if you have access.": "後でもう一度試すか、あなたがアクセスできるかどうか部屋の管理者に問い合わせてください。",
+    "This room doesn't exist. Are you sure you're at the right place?": "この部屋は存在しません。表示内容が正しくない可能性があります?",
+    "You're previewing %(roomName)s. Want to join it?": "部屋 %(roomName)s のプレビューです。参加したいですか?",
+    "Share this email in Settings to receive invites directly in %(brand)s.": "このメールアドレスを設定から共有すると、%(brand)s から招待を受け取れます。",
+    "Use an identity server in Settings to receive invites directly in %(brand)s.": "設定から identity サーバーを使うと、%(brand)s から直接招待を受け取れます。",
+    "This invite to %(roomName)s was sent to %(email)s": "部屋 %(roomName)s への正体はメールアドレス %(email)s へ送られました",
+    "Link this email with your account in Settings to receive invites directly in %(brand)s.": "このメールアドレスを設定からあなたのアカウントにリンクすると %(brand)s から直接招待を受け取ることができます。",
+    "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "部屋 %(roomName)s への招待はアカウントに関連付けられていないメールアドレス %(email)s に送られました",
+    "You can still join it because this is a public room.": "公開された部屋なので参加が可能です。",
+    "Try to join anyway": "とにかく参加してみる",
+    "You can only join it with a working invite.": "有効な招待がある場合にのみ参加できます。",
+    "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "招待を検証しようとした際にエラー(%(errcode)s)が発生しました。この情報を部屋の管理者に伝えてみてはどうでしょうか。",
+    "Something went wrong with your invite to %(roomName)s": "%(roomName)s への招待に問題が発生しました",
+    "You were banned from %(roomName)s by %(memberName)s": "%(memberName)s により %(roomName)s からあなたは禁止されました",
+    "Re-join": "再参加",
+    "Reason: %(reason)s": "理由: %(reason)s",
+    "You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s により %(roomName)s からあなたは蹴り出されました",
+    "Loading room preview": "部屋プレビューのロード中",
+    "Sign Up": "サインアップ",
+    "Join the conversation with an account": "アカウントで会話に参加する",
+    "Rejecting invite …": "招待を拒否する…",
+    "%(count)s results|one": "%(count)s 件の結果",
+    "%(count)s results|other": "%(count)s 件の結果",
+    "Use the + to make a new room or explore existing ones below": "+を使って新しい部屋を作成するか、以下の既存の部屋を探索します",
+    "Explore all public rooms": "すべての公開ルームを探索する",
+    "Start a new chat": "新しいチャットを開始します",
+    "Can't see what you’re looking for?": "探しているものが見つかりませんか?",
+    "Custom Tag": "カスタムタグ",
+    "Explore public rooms": "公開ルームを探索する",
+    "Discovery options will appear once you have added a phone number above.": "上記の電話番号を追加すると Discovery オプションが表示されます。",
+    "Verification code": "確認コード",
+    "Please enter verification code sent via text.": "テキストで送信された確認コードを入力してください。",
+    "Unable to verify phone number.": "電話番号を検証できません。",
+    "Unable to share phone number": "電話番号を共有できません",
+    "Unable to revoke sharing for phone number": "電話番号の共有を取り消せません",
+    "Discovery options will appear once you have added an email above.": "上記のメールを追加すると Discovery オプションが表示されます。",
+    "Share": "共有",
+    "Revoke": "取り消す",
+    "Complete": "完了",
+    "Verify the link in your inbox": "受信したメールの検証リンクを開いてください",
+    "Click the link in the email you received to verify and then click continue again.": "受信したメール中のリンクを開いて検証して、その後に「続ける」を押します。",
+    "Your email address hasn't been verified yet": "あなたのメールアドレスはまだ検証されていません",
+    "Unable to share email address": "メールアドレスを共有できません",
+    "Unable to revoke sharing for email address": "メールアドレスの共有を取り消せません",
+    "To link to this room, please add an address.": "この部屋にリンクするにはアドレスを追加してください。",
+    "Send %(eventType)s events": "%(eventType)s イベントを送信します",
+    "Remove messages sent by others": "他の人から送信されたメッセージを削除する",
+    "An error occurred changing the user's power level. Ensure you have sufficient permissions and try again.": "ユーザーのパワーレベルの変更中にエラーが発生しました。 十分な権限があることを確認して、再試行してください。",
+    "Uploaded sound": "アップロードされたサウンド",
+    "Bridges": "ブリッジ",
+    "This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "この部屋はどのプラットフォームともメッセージをブリッジしていません。<a>詳細</a>",
+    "This room is bridging messages to the following platforms. <a>Learn more.</a>": "この部屋は以下のプラットフォームにメッセージをブリッジしています。 <a>詳細</a>",
+    "View older messages in %(roomName)s.": "%(roomName)s の古いメッセージを表示します。",
+    "this room": "この部屋",
+    "Upgrade this room to the recommended room version": "この部屋を推奨部屋バージョンにアップグレードします",
+    "<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members to the new version of the room.</i> We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "<b>警告</b>: ルームをアップグレードしても、<i>ルームメンバーが新しいバージョンのルームに自動的に移行されることはありません。</i> 古いバージョンのルームの新しいルームへのリンクを投稿します。ルームメンバーは、このリンクをクリックして新しいルームに参加する必要があります。",
+    "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "サーバー管理者は、プライベートな部屋とダイレクトメッセージでデフォルトでエンドツーエンド暗号化を無効にしています。",
+    "Accept all %(invitedRooms)s invites": "%(invitedRooms)s の招待を全て受け入れる",
+    "Read Marker off-screen lifetime (ms)": "既読マーカーを動かすまでの時間(画面オフ時)(ミリ秒)",
+    "Read Marker lifetime (ms)": "既読マーカーを動かすまでの時間(ミリ秒)",
+    "Subscribe": "購読",
+    "Room ID or address of ban list": "禁止リストの部屋IDまたはアドレス",
+    "If this isn't what you want, please use a different tool to ignore users.": "これが希望どおりでない場合は、別のツールを使用してユーザーを無視してください。",
+    "Subscribing to a ban list will cause you to join it!": "禁止リストを購読するとあなたはその効果が得られます!",
+    "Subscribed lists": "購読リスト",
+    "eg: @bot:* or example.org": "例: @bot:* や example.org など",
+    "Server or user ID to ignore": "無視するサーバー/ユーザー ID",
+    "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "あなたの個人的な禁止リストはあなたがメッセージを見たくないすべてのユーザー/サーバーを保持します。 最初のユーザー/サーバーを無視すると、「マイ禁止リスト」という名前の新しい部屋が部屋リストに表示されます。この部屋から出ると禁止リストは効果を失います。",
+    "Personal ban list": "個人的な禁止リスト",
+    "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "人を無視することは、誰を禁止するかについての規則を含む禁止リストを通して行われます。 禁止リストに登録すると、そのリストによってブロックされたユーザー/サーバーが非表示になります。",
+    "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "無視するユーザー/サーバーをここに追加します。 アスタリスクを使用して %(brand)s を任意の文字と一致させます。たとえば、 <code>@bot:*</code> は任意のサーバーで「bot」という名前のすべてのユーザーを無視します。",
+    "⚠ These settings are meant for advanced users.": "⚠これらの設定は、上級ユーザーを対象としています。",
+    "You are currently subscribed to:": "購読されています:",
+    "View rules": "ルールを表示",
+    "Unsubscribe": "購読の解除",
+    "You are not subscribed to any lists": "あなたはどのリストにも加入していません",
+    "You are currently ignoring:": "あなたは無視しています:",
+    "You have not ignored anyone.": "あなたは誰も無視していません。",
+    "User rules": "ユーザールール",
+    "Server rules": "サーバールール",
+    "Ban list rules - %(roomName)s": "禁止ルールのリスト - %(roomName)s",
+    "None": "なし",
+    "Please try again or view your console for hints.": "もう一度試すか、コンソールでヒントを確認してください。",
+    "Error unsubscribing from list": "リスト購読解除のエラー",
+    "Error removing ignored user/server": "ユーザー/サーバーの無視を除去する際のエラー",
+    "Please verify the room ID or address and try again.": "部屋のIDやアドレスを確認して、もう一度お試しください。",
+    "Error subscribing to list": "リスト購読のエラー",
+    "Something went wrong. Please try again or view your console for hints.": "何かがうまくいかなかった。 もう一度試すか、コンソールでヒントを確認してください。",
+    "Error adding ignored user/server": "ユーザー/サーバーの無視を追加する際のエラー",
+    "Ignored/Blocked": "無視/ブロック",
+    "Customise your experience with experimental labs features. <a>Learn more</a>.": "試験機能を使って利用経験を調整します。 <a>もっと見る</a>。",
+    "Chat with %(brand)s Bot": "%(brand)s ボットとチャットする",
+    "For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "%(brand)s の使用についてサポートが必要な場合は、 <a>こちら</a> をクリックするか、下のボタンを使用してボットとチャットを開始してください。",
+    "Discovery": "見つける",
+    "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "identity サーバー (%(serverName)s) の利用規約に同意して、メールアドレスや電話番号でユーザを見つけたり見つけられたり招待したりできるようにします。",
+    "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "パスワードは正常に変更されました。 他のセッションに再度ログインするまで、他のセッションでプッシュ通知を受信しません",
+    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "システムにインストールされているフォントの名前を設定すると、 %(brand)s がそれを使おうとします。",
+    "Theme added!": "テーマが追加されました!",
+    "Error downloading theme information.": "テーマ情報のダウンロード中にエラーが発生しました。",
+    "Invalid theme schema.": "テーマスキーマが無効です。",
+    "Use between %(min)s pt and %(max)s pt": "%(min)s ~ %(max)s (pt)の間の数字を指定します",
+    "Custom font size can only be between %(min)s pt and %(max)s pt": "カスタムフォントサイズの指定(単位: point)は %(min)s ~ %(max)s の間にできます",
+    "Size must be a number": "サイズには数値を指定してください",
+    "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.": "identity サーバーから切断すると、連絡先を使ってユーザを見つけたり見つけられたり招待したりできなくなります。",
+    "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "現在 identity サーバーを使用していません。連絡先を使ってユーザを見つけたり見つけられたりするには identity サーバーを以下に追加します。",
+    "Identity Server": "identity サーバー",
+    "If you don't want to use <server /> to discover and be discoverable by existing contacts you know, enter another identity server below.": "連絡先の検出に <server /> ではなく他の identity サーバーを使いたい場合は以下に指定してください。",
+    "You are currently using <server></server> to discover and be discoverable by existing contacts you know. You can change your identity server below.": "現在 <server></server> を使用して、連絡先を検出可能にしています。以下で identity サーバーを変更できます。",
+    "Identity Server (%(server)s)": "identity サーバー (%(server)s)",
+    "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "切断する前に、identity サーバーからメールアドレスと電話番号を削除することをお勧めします。",
+    "You are still <b>sharing your personal data</b> on the identity server <idserver />.": "まだ identity サーバー <idserver /> で<b>個人データを共有</b>しています。",
+    "Disconnect anyway": "とにかく切断します",
+    "wait and try again later": "しばらく待って、後でもう一度試す",
+    "contact the administrators of identity server <idserver />": "identity サーバー <idserver /> の管理者に連絡する",
+    "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "identity サーバーをブロックする可能性のあるもの(Privacy Badgerなど)がないか、ブラウザプラグインを確認してください",
+    "You should:": "するべきこと:",
+    "You should <b>remove your personal data</b> from identity server <idserver /> before disconnecting. Unfortunately, identity server <idserver /> is currently offline or cannot be reached.": "切断する前に identity サーバー <idserver /> から<b>個人データを削除</b>する必要があります。 しかし残念ながら identity サーバー <idserver /> は現在オフライン状態か、またはアクセスできません。",
+    "Disconnect": "切断する",
+    "Disconnect from the identity server <idserver />?": "identity サーバー <idserver /> から切断しますか?",
+    "Disconnect identity server": "identity サーバーを切断します",
+    "The identity server you have chosen does not have any terms of service.": "選択した identity サーバーには利用規約がありません。",
+    "Terms of service not accepted or the identity server is invalid.": "利用規約に同意していないか、identity サーバーが無効です。",
+    "Disconnect from the identity server <current /> and connect to <new /> instead?": "identity サーバー <current /> から切断して <new /> に接続しますか?",
+    "Change identity server": "identity サーバーを変更する",
+    "Checking server": "サーバーをチェックしています",
+    "Could not connect to Identity Server": "identity サーバーに接続できませんでした",
+    "Not a valid Identity Server (status code %(code)s)": "有効な identity サーバーではありません (ステータスコード %(code)s)",
+    "Identity Server URL must be HTTPS": "identityサーバーのURLは HTTPS スキーマである必要があります",
+    "not ready": "準備ができていない",
+    "ready": "準備ができました",
+    "unexpected type": "unexpected type",
+    "well formed": "well formed",
+    "Your keys are <b>not being backed up from this session</b>.": "キーは<b>このセッションからバックアップされていません</b>。",
+    "Backing up %(sessionsRemaining)s keys...": "%(sessionsRemaining)s キーをバックアップしています…",
+    "Unable to load key backup status": "キーのバックアップ状態を読み込めません",
+    "The operation could not be completed": "操作を完了できませんでした",
+    "Failed to save your profile": "プロファイルの保存に失敗しました",
+    "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "%(brand)s 以外のクライアントでそれらを構成した可能性があります。%(brand)sで変更することはできませんが適用されます。",
+    "There are advanced notifications which are not shown here.": "ここに表示されていない追加の通知があります。",
+    "Clear notifications": "通知をクリアする",
+    "The integration manager is offline or it cannot reach your homeserver.": "integration マネージャーがオフライン状態か、またはあなたのホームサーバに到達できません。",
+    "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "Webブラウザー上で動作する %(brand)s Web は暗号化メッセージの安全なキャッシュをローカルに保存できません。<desktopLink>%(brand)s Desktop</desktopLink> アプリを使うと暗号化メッセージを検索結果に表示することができます。",
+    "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "暗号化されたメッセージの安全なキャッシュをローカルに保存するためのいくつかのコンポーネントが %(brand)s にはありません。 この機能を試してみたい場合は、<nativeLink>検索コンポーネントが追加された </nativeLink> %(brand)s デスクトップのカスタム版をビルドしてください。",
+    "Securely cache encrypted messages locally for them to appear in search results.": "暗号化メッセージの安全なキャッシュをローカルに保存して、検索結果に表示できるようにします。",
+    "Delete sessions|one": "セッションを削除する",
+    "Delete sessions|other": "セッションを削除する",
+    "Click the button below to confirm deleting these sessions.|one": "下のボタンをクリックしてこのセッションの削除を確認してください。",
+    "Click the button below to confirm deleting these sessions.|other": "下のボタンをクリックしてこれらのセッションの削除を確認してください。",
+    "Confirm deleting these sessions": "これらのセッションの削除を確認してください",
+    "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "あなたのidentityを確認するためシングルサインオンを使いこのセッションを削除することを確認します。",
+    "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "あなたのidentityを調べるためにシングルサインオンを使いこれらのセッションを削除することを確認します。",
+    "not found in storage": "ストレージには見つかりません",
+    "Set up": "設定する",
+    "Cross-signing is not set up.": "クロス署名が設定されていません。",
+    "Passwords don't match": "パスワードが一致しません",
+    "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.": "パスワードを変更すると全てのセッションでエンドツーエンド暗号化キーがリセットされて暗号化されたチャット履歴が読み取れなくなります。将来的にはこれは改善される見込みですが、現時点では、パスワード変更の前に部屋のキーをエクスポートして後で再インポートすることを検討してください。",
+    "Channel: <channelLink/>": "Channel: <channelLink/>",
+    "Workspace: <networkLink/>": "Workspace: <networkLink/>",
+    "This bridge is managed by <user />.": "このブリッジは<user />により管理されています。",
+    "This bridge was provisioned by <user />.": "このブリッジは<user />により提供されました。",
+    "Decline (%(counter)s)": "Decline (%(counter)s)",
+    "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
+    "Your server isn't responding to some <a>requests</a>.": "あなたのサーバは数回の<a>リクエスト</a>に応答しません。",
+    "Pin": "ピン",
+    "Folder": "フォルダー",
+    "Headphones": "ヘッドホン",
+    "Anchor": "錨",
+    "Bell": "鐘",
+    "Trumpet": "トランペット",
+    "Guitar": "ギター",
+    "Ball": "ボール",
+    "Trophy": "トロフィー",
+    "Rocket": "ロケット",
+    "Aeroplane": "飛行機",
+    "Bicycle": "自転車",
+    "Train": "列車",
+    "Flag": "旗",
+    "Telephone": "電話",
+    "Hammer": "ハンマー",
+    "Key": "鍵",
+    "Lock": "錠前",
+    "Scissors": "鋏",
+    "Paperclip": "紙ばさみ",
+    "Pencil": "鉛筆",
+    "Book": "本",
+    "Light bulb": "電球",
+    "Gift": "ギフト",
+    "Clock": "時計",
+    "Hourglass": "砂時計",
+    "Umbrella": "傘",
+    "Thumbs up": "サムズアップ",
+    "Santa": "サンタ",
+    "Spanner": "スパナ",
+    "Glasses": "眼鏡",
+    "Hat": "帽子",
+    "Robot": "ロボット",
+    "Smiley": "笑顔",
+    "Heart": "ハート",
+    "Cake": "ケーキ",
+    "Pizza": "ピザ",
+    "Corn": "トウモロコシ",
+    "Strawberry": "苺",
+    "Apple": "林檎",
+    "Banana": "バナナ",
+    "Fire": "炎",
+    "Cloud": "雲",
+    "Moon": "月",
+    "Globe": "金魚鉢",
+    "Mushroom": "茸",
+    "Cactus": "サボテン",
+    "Tree": "木",
+    "Flower": "花",
+    "Butterfly": "蝶",
+    "Octopus": "蛸",
+    "Fish": "魚",
+    "Turtle": "亀",
+    "Penguin": "ペンギン",
+    "Rooster": "鶏",
+    "Panda": "パンダ",
+    "Rabbit": "兎",
+    "Elephant": "象",
+    "Pig": "豚",
+    "Unicorn": "一角獣",
+    "Horse": "馬",
+    "Lion": "ライオン",
+    "Cat": "猫",
+    "Dog": "犬",
+    "To be secure, do this in person or use a trusted way to communicate.": "安全を確保するため、1人でこれを行うか、または信頼できる方法で連携してください。",
+    "They don't match": "それらは一致しません",
+    "They match": "それらは一致します",
+    "Cancelling…": "取り消し中…",
+    "Waiting for your other session to verify…": "他のセッションによる検証を待っています…",
+    "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "他のセッション %(deviceName)s (%(deviceId)s) による検証を待っています…",
+    "Unable to find a supported verification method.": "どの検証方法にも対応していません。",
+    "Verify this user by confirming the following number appears on their screen.": "このユーザを検証するため、両方の画面に同じ番号が表示されていることを確認してください。",
+    "The user must be unbanned before they can be invited.": "招待する前にユーザの禁止を解除する必要があります。",
+    "Unrecognised address": "認識されないアドレス",
+    "Error leaving room": "部屋を出る際のエラー",
+    "Unexpected server error trying to leave the room": "部屋を出る際に予期しないサーバーエラー",
+    "The message you are trying to send is too large.": "送信しようとしているメッセージが大きすぎます。",
+    "Unexpected error resolving identity server configuration": "identity サーバー構成の解釈中に予期しないエラーが発生しました",
+    "Unexpected error resolving homeserver configuration": "ホームサーバー構成の解釈中に予期しないエラーが発生しました",
+    "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "あなたはログインできますが、identity サーバーがオンラインに戻るまで一部の機能を使用できません。 この警告が引き続き表示される場合は、構成を確認するか、サーバー管理者に連絡してください。",
+    "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "あなたはパスワードをリセットできますが、identity サーバーがオンラインに復帰するまで一部の機能を使用できません。 この警告が引き続き表示される場合は、構成を確認するか、サーバー管理者に連絡してください。",
+    "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "あなたは登録できますが、identity サーバーがオンラインに復帰するまで一部の機能は使用できません。 この警告が引き続き表示される場合は、構成を確認するか、サーバー管理者に連絡してください。",
+    "Ask your %(brand)s admin to check <a>your config</a> for incorrect or duplicate entries.": "<a>設定</a>が間違っているか重複しているか確認するよう、%(brand)s の管理者に問い合わせてください。",
+    "Ensure you have a stable internet connection, or get in touch with the server admin": "安定したインターネット接続があることを確認するか、サーバー管理者に連絡してください",
+    "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 this room": "部屋に送られた動画を見る",
+    "See videos posted to your active 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, message, or user you're viewing": "表示する部屋/メッセージ/ユーザを変える",
+    "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": "他の部屋を表示してる間も実行中は画面に留める",
+    "Ask this user to verify their session, or manually verify it below.": "このユーザーに彼らのセッションを検証するよう問い合わせるか、以下のように手動で検証してください。",
+    "Verify your other session using one of the options below.": "以下のどれか一つを使って他のセッションを検証します。",
+    "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)sは禁止ルール %(oldGlob)s を %(newGlob)s (理由 %(reason)s)に変更しました",
+    "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s はサーバ禁止ルール %(oldGlob)s を %(newGlob)s (理由 %(reason)s) に変更しました",
+    "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s は部屋の禁止ルール %(oldGlob)s を %(newGlob)s (理由 %(reason)s) に変更しました",
+    "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s はユーザ禁止ルール %(oldGlob)s を %(newGlob)s (理由 %(reason)s) に変更しました",
+    "%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s は禁止ルール %(glob)s (理由 %(reason)s)を作成しました",
+    "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s はサーバ禁止ルール %(glob)s (理由 %(reason)s)を作成しました",
+    "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s は部屋の禁止ルール %(glob)s (理由 %(reason)s)を作成しました",
+    "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s はユーザ禁止ルール %(glob)s (理由 %(reason)s)を作成しました",
+    "%(senderName)s updated a ban rule 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 updated the rule banning users matching %(glob)s for %(reason)s": "%(senderName)s はユーザ禁止ルール %(glob)s (理由 %(reason)s)を更新しました",
+    "%(senderName)s updated an invalid ban rule": "%(senderName)s はinvalidな禁止ルールを更新しました",
+    "%(senderName)s removed a ban rule matching %(glob)s": "%(senderName)s は 禁止ルール %(glob)s を削除しました",
+    "%(senderName)s removed the rule banning servers matching %(glob)s": "%(senderName)s はサーバ禁止ルール %(glob)s を削除しました",
+    "%(senderName)s removed the rule banning rooms matching %(glob)s": "%(senderName)s は部屋の禁止ルール %(glob)s を削除しました",
+    "%(senderName)s removed the rule banning users matching %(glob)s": "%(senderName)s はユーザー禁止ルール %(glob)s を削除しました",
+    "%(senderName)s has updated the widget layout": "%(senderName)s はウィジェットのレイアウトを更新しました",
+    "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s は部屋 %(targetDisplayName)s への招待を取り消しました。",
+    "%(senderName)s declined the call.": "%(senderName)s は通話を拒否しました。",
+    "(an error occurred)": "(エラーが発生しました)",
+    "(their device couldn't start the camera / microphone)": "(彼らのデバイスはカメラ/マイクを使用できませんでした)",
+    "(connection failed)": "(接続に失敗しました)",
+    "%(senderName)s changed the addresses for this room.": "%(senderName)s がこの部屋のアドレスを変更しました。",
+    "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s がこの部屋のメインアドレスと代替アドレスを変更しました。",
+    "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s がこの部屋の代替アドレスを変更しました。",
+    "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s がこの部屋の代替アドレス %(addresses)s を削除しました。",
+    "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s がこの部屋の代替アドレス %(addresses)s を削除しました。",
+    "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s がこの部屋の代替アドレス %(addresses)s を追加しました。",
+    "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s がこの部屋の代替アドレス %(addresses)s を追加しました。",
+    "🎉 All servers are banned from participating! This room can no longer be used.": "🎉すべてのサーバーは参加を禁止されています! この部屋は使用できなくなりました。",
+    "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s がこの部屋のサーバーACLを変更しました。",
+    "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s がこの部屋のサーバーACLを設定しました。",
+    "Converts the DM to a room": "DMを部屋に変換します",
+    "Converts the room to a DM": "部屋をDMに変換します",
+    "Takes the call in the current room off hold": "現在の部屋の通話を保留から外します",
+    "Places the call in the current room on hold": "保留中の現在の部屋に通話を発信します",
+    "Sends a message to the given user": "指定されたユーザーにメッセージを送ります",
+    "Opens chat with the given user": "指定されたユーザーとのチャットを開きます",
+    "Send a bug report with logs": "ログ付きのバグ報告を送る",
+    "Displays information about a user": "ユーザーの情報を表示します",
+    "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "指定された署名キーは %(userId)s のセッション %(deviceId)s から受け取ったキーと一致します。セッションは検証済みです。",
+    "Unknown (user, session) pair:": "ユーザーとセッションのペアが不明です:",
+    "Verifies a user, session, and pubkey tuple": "ユーザー、セッション、およびpubkeyタプルを検証します",
+    "Please supply a widget URL or embed code": "ウィジェットのURLまたは埋め込みコードを入力してください",
+    "Could not find user in room": "部屋にユーザーが見つかりません",
+    "Command failed": "コマンドが失敗しました",
+    "Unrecognised room address:": "部屋のアドレスを認識できません:",
+    "Joins room with given address": "指定されたアドレスの部屋に参加します",
+    "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "メールでの招待に identity サーバーを使います。デフォルトの identity サーバー (%(defaultIdentityServerName)s) を使う場合は「続ける」を押してください。または設定画面を開いて変更してください。",
+    "Failed to set topic": "トピックの設定に失敗しました",
+    "Double check that your server supports the room version chosen and try again.": "選択した部屋のバージョンをあなたのサーバーがサポートしているか何度も確認してから、もう一度試してください。",
+    "Sends a message as html, without interpreting it as markdown": "メッセージを(Markdownではなく)HTMLとして送信します",
+    "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "プレーンテキストメッセージの前に ( ͡° ͜ʖ ͡°) を付けます",
+    "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "プレーンテキストメッセージの前に ┬──┬ ノ( ゜-゜ノ) を付けます",
+    "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "プレーンテキストメッセージの前に (╯°□°)╯︵ ┻━┻ を付けます",
+    "Effects": "効果",
+    "Setting up keys": "キーのセットアップ",
+    "Are you sure you want to cancel entering passphrase?": "パスフレーズの入力をキャンセルしてもよいですか?",
+    "Cancel entering passphrase?": "パスフレーズの入力をキャンセルしますか?",
+    "Custom (%(level)s)": "カスタム(%(level)s)",
+    "Use your account or create a new one to continue.": "作成ずみのアカウントを使うか、新しいアカウントを作成して続けます。",
+    "Sign In or Create Account": "サインインまたはアカウントの作成",
+    "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": "南ジョージア&南サンドイッチ諸島",
+    "Explore community rooms": "コミュニティルームを探索する",
+    "Open dial pad": "ダイヤルパッドを開く",
+    "Start a Conversation": "会話を始める",
+    "Show Widgets": "ウィジェットを表示する",
+    "Hide Widgets": "ウィジェットを隠す",
+    "No recently visited rooms": "最近訪れた部屋はありません",
+    "Recently visited rooms": "最近訪れた部屋",
+    "Room %(name)s": "部屋 %(name)s",
+    "Code block": "コードブロック",
+    "Strikethrough": "取り消し線",
+    "The authenticity of this encrypted message can't be guaranteed on this device.": "この暗号化メッセージの信頼性はこのデバイスでは保証できません。",
+    "Mod": "Mod",
+    "Edit message": "メッセージの編集",
+    "Someone is using an unknown session": "誰かが不明なセッションを使用しています",
+    "You have verified this user. This user has verified all of their sessions.": "このユーザーを検証しました。このユーザは全てのセッションを検証しました。",
+    "You have not verified this user.": "あなたはこのユーザーを検証していません。",
+    "This user has not verified all of their sessions.": "このユーザーはすべてのセッションを確認していません。",
+    "Phone Number": "電話番号",
+    "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "+%(msisdn)s にテキストメッセージを送りました。メッセージに含まれた確認コードを入力してください。",
+    "Remove %(phone)s?": "%(phone)s を除去しますか?",
+    "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "アドレスを確認するためメールを送りました。そこにある手順に従い、その後に下のボタンを押してください。",
+    "Remove %(email)s?": "%(email)s を除去しますか?",
+    "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": "フェロー諸島",
+    "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": "コンゴ-ブラザビル",
+    "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": "イギリス",
+    "%(name)s is requesting verification": "%(name)s は検証を要望しています",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "ホームサーバーがログインの試行を拒否しました。時間がかかりすぎたことが原因かもしれません。もう一度やり直してください。これが続く場合はホームサーバー管理者に連絡してください。",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "ホームサーバーにアクセスできませんでした。もう一度やり直してください。これが続く場合はホームサーバー管理者に連絡してください。",
+    "Try again": "もう一度試す",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "残念ながらブラウザはサインインするホームサーバーを忘れてしまいました。 サインインページに移動して再試行してください。",
+    "We couldn't log you in": "ログインできませんでした",
+    "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "このアクションはデフォルトのidentity サーバー <server /> にアクセスしてメールアドレスまたは電話番号を検証する必要がありますが、サーバには利用規約がありません。",
+    "Room name or address": "部屋の名前またはアドレス",
+    "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "現時点ではファイルで返信することはできません。 返信せずにこのファイルをアップロードしますか?",
+    "This will end the conference for everyone. Continue?": "これは全員の会議を終了させます。続けますか?",
+    "End conference": "会議を終了する",
+    "You've reached the maximum number of simultaneous calls.": "同時通話数の上限に達しました。",
+    "Too Many Calls": "通話が多すぎます",
+    "No other application is using the webcam": "他のアプリがWebカメラを使用中ではないこと",
+    "Permission is granted to use the webcam": "Webカメラを使う権限が与えられていること",
+    "A microphone and webcam are plugged in and set up correctly": "マイクとWebカメラが接続されていて、正しく設定されていること",
+    "Verify this user by confirming the following emoji appear on their screen.": "このユーザを検証するため、両方の画面に絵文字が同じ順序で表示されていることを確認してください。",
+    "Verify this session by confirming the following number appears on its screen.": "このセッションを検証するため、同じ番号が両方の画面に表示されていることを確認してください。",
+    "Confirm the emoji below are displayed on both sessions, in the same order:": "以下の絵文字が両方のセッションで同じ順序で表示されていることをしてください:",
+    "Start": "開始",
+    "Compare a unique set of emoji if you don't have a camera on either device": "両方の端末でQRコードをキャプチャできない場合、絵文字の比較を選んでください",
+    "Compare unique emoji": "絵文字の並びを比較する",
+    "or": "または",
+    "Scan this unique code": "ユニークなコードをスキャン",
+    "Verify this session by completing one of the following:": "以下のどれか一つの方法で、このセッションを検証します。",
+    "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "ユーザ間でエンドツーエンド暗号化されたメッセージです。第三者が解読することはできません。",
+    "Verified!": "検証されました!",
+    "The other party cancelled the verification.": "相手方が確認をキャンセルしました。",
+    "Incoming call": "着信",
+    "Incoming video call": "ビデオ通話の着信",
+    "Incoming voice call": "音声通話の着信",
+    "Unknown caller": "不明な発信者",
+    "Dial pad": "ダイヤルパッド",
+    "There was an error looking up the phone number": "電話番号を見つける際にエラーがありました",
+    "Unable to look up phone number": "電話番号が見つかりません",
+    "%(name)s on hold": "%(name)s が保留中",
+    "Return to call": "電話に戻る",
+    "Fill Screen": "全画面",
+    "Voice Call": "音声電話",
+    "Video Call": "ビデオ通話",
+    "%(peerName)s held the call": "%(peerName)s が電話をかけました",
+    "You held the call <a>Resume</a>": "<a>再開</a>の電話をかけました",
+    "You held the call <a>Switch</a>": "<a>スイッチ</a>に電話をかけました",
+    "sends snowfall": "降雪を送る",
+    "Sends the given message with snowfall": "メッセージを降雪と共に送る",
+    "sends fireworks": "花火を送る",
+    "Sends the given message with fireworks": "メッセージを花火と共に送る",
+    "sends confetti": "紙吹雪を送る",
+    "Sends the given message with confetti": "メッセージを紙吹雪と共に送信します",
+    "This is your list of users/servers you have blocked - don't leave the room!": "あなたがブロックしたユーザー/サーバーのリストです。部屋から出ないでください!",
+    "My Ban List": "私の禁止リスト",
+    "Downloading logs": "ログのダウンロード",
+    "Uploading logs": "ログのアップロード",
+    "Show chat effects (animations when receiving e.g. confetti)": "チャット効果を表示する(紙吹雪などを受け取ったときのアニメーション)",
+    "IRC display name width": "IRC表示名の幅",
+    "How fast should messages be downloaded.": "メッセージをダウンロードする速度。",
+    "Enable message search in encrypted rooms": "暗号化された部屋でもメッセージ検索を有効にする",
+    "Show hidden events in timeline": "省略されたイベントをタイムラインに表示する",
+    "Use Command + Enter to send a message": "メッセージ送信に Command + Enter を使う",
+    "Use Command + F to search": "検索に Command + F を使う",
+    "Show line numbers in code blocks": "コードブロックに行番号を表示する",
+    "Expand code blocks by default": "デフォルトでコードブロックを展開表示する",
+    "Show stickers button": "ステッカーボタンを表示する",
+    "Show info about bridges in room settings": "部屋の設定にブリッジの情報を表示する",
+    "Enable advanced debugging for the room list": "ルーム一覧の高度なデバッグを有効にする",
+    "Offline encrypted messaging using dehydrated devices": "dehydrated デバイスを使用したオフライン暗号化メッセージング",
+    "Show message previews for reactions in all rooms": "すべての部屋でリアクションのメッセージプレビューを表示する",
+    "Show message previews for reactions in DMs": "DM中のリアクションにメッセージプレビューを表示する",
+    "Support adding custom themes": "カスタムテーマの追加に対応する",
+    "Try out new ways to ignore people (experimental)": "人々を無視する新しい方法を試す (実験的)",
+    "Multiple integration managers": "複数の integration マネージャー",
+    "Group & filter rooms by custom tags (refresh to apply changes)": "カスタムタグを使って部屋をグループまたはフィルタします(ページのリロードが必要)",
+    "New spinner design": "新しいスピナーのデザイン",
+    "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. 互換性のあるホームサーバーが必要です。 非常に実験的。注意して使用してください。",
+    "Render LaTeX maths in messages": "メッセージ中の LaTeX 数式を描画する",
+    "Change notification settings": "通知設定を変更する",
+    "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
+    "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
+    "%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
+    "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s",
+    "%(senderName)s is calling": "%(senderName)s が呼び出し中",
+    "Waiting for answer": "応答を待っています",
+    "%(senderName)s started a call": "%(senderName)s が通話を開始しました",
+    "You started a call": "あなたは通話を開始しました",
+    "Call ended": "通話が終了しました",
+    "%(senderName)s ended the call": "%(senderName)s が通話を終了しました",
+    "You ended the call": "あなたは通話を終了しました",
+    "Call in progress": "通話中",
+    "%(senderName)s joined the call": "%(senderName)s が通話に参加しました",
+    "You joined the call": "通話に参加しました",
+    "The person who invited you already left the room, or their server is offline.": "あなたを招待した人はすでに部屋を出たか、彼らのサーバーがオフライン状態です。",
+    "The person who invited you already left the room.": "あなたを招待した人はすでに部屋を出ました。",
+    "There was an error joining the room": "部屋に参加する際にエラーがありました",
+    "Guest": "ゲスト",
+    "Verify the new login accessing your account: %(name)s": "あなたのアカウントへの新しいログインを確認します: %(name)s",
+    "New login. Was this you?": "新しいログインがありました。これはあなたですか?",
+    "Safeguard against losing access to encrypted messages & data": "暗号化されたメッセージとデータへのアクセスを失うことから保護します",
+    "Ok": "OK",
+    "Contact your <a>server admin</a>.": "<a>サーバ管理者</a>に問い合わせてください。",
+    "Your homeserver has exceeded one of its resource limits.": "ホームサーバーはリソースの上限に達しました。",
+    "Your homeserver has exceeded its user limit.": "あなたのホームサーバーはユーザー数の上限に達しました。",
+    "Use app": "アプリを使う",
+    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Webはモバイル端末ではまだ試験的です。より良い体験と最新の機能を得るには、無料のネイティブアプリを使用してください。",
+    "Use app for a better experience": "より良い体験のためにアプリを使用する",
+    "Enable": "有効",
+    "Enable desktop notifications": "デスクトップ通知を有効にする",
+    "Don't miss a reply": "返信をお見逃しなく",
+    "Verify all your sessions to ensure your account & messages are safe": "すべてのセッションを確認して、アカウントとメッセージが安全であることを確認します",
+    "Review where you’re logged in": "どこからログインしたか確認する",
+    "No": "いいえ",
+    "Yes": "はい",
+    "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "%(brand)s の向上に役立つ <UsageDataLink>匿名の使用状況データ</UsageDataLink> を送信します。 これは <PolicyLink>cookie</PolicyLink> を使用します。",
+    "Help us improve %(brand)s": "%(brand)s の改善にご協力ください",
+    "Short keyboard patterns are easy to guess": "短いキーボードパターンは簡単に推測されます",
+    "Straight rows of keys are easy to guess": "キーの配置順序を辿ると簡単に推測されます",
+    "Common names and surnames are easy to guess": "名前と名字は簡単に推測されます",
+    "Names and surnames by themselves are easy to guess": "名前や名字は簡単に推測されます",
+    "Call failed because webcam or microphone could not be accessed. Check that:": "Webカメラやマイクを利用できず通話に失敗しました。確認してください:",
+    "Unable to access webcam / microphone": "Webカメラとマイクを利用できません",
+    "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "マイクを利用できなかったので通話に失敗しました。マイクが接続されて正しく設定されているか確認してください。",
+    "Unable to access microphone": "マイクを利用できません",
+    "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "または公開サーバー <code>turn.matrix.org</code> を使用することもできますが、信頼性は低く、また公開サーバにIPアドレスが漏洩します。これはアプリ設定でも変更できます。",
+    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "通話が機能するよう、ホームサーバー(<code>%(homeserverDomain)s</code>)の管理者にTURNサーバーの設定を尋ねてください。",
+    "The call was answered on another device.": "通話は他の端末で応答されました。",
+    "Answered Elsewhere": "他端末で応答しました",
+    "The call could not be established": "通話を確立できませんでした",
+    "The other party declined the call.": "相手方は通話を拒否しました。",
+    "Call Declined": "通話は拒否されました",
+    "Whether you're using %(brand)s as an installed Progressive Web App": "インストールされた Progressive Web App として %(brand)s を使用しているか",
+    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "主な入力方法がタッチであるデバイスで %(brand)s を使っているか",
+    "Click the button below to confirm adding this phone number.": "下のボタンをクリックして電話番号の追加を確認します。",
+    "Confirm adding phone number": "電話番号の追加を確認する",
+    "Confirm adding this phone number by using Single Sign On to prove your identity.": "シングルサインオンを使用して本人確認を行い、電話番号の追加を承認してください。",
+    "Click the button below to confirm adding this email address.": "下のボタンを押してこのメールアドレスを確認します。"
 }

From bfa47ba447c3fc5800b6e78349d57d3a9c305003 Mon Sep 17 00:00:00 2001
From: Thibault Martin <mail@thibaultmart.in>
Date: Tue, 9 Mar 2021 12:24:53 +0000
Subject: [PATCH 366/389] Translated using Weblate (French)

Currently translated at 100.0% (2780 of 2780 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 | 898 +++++++++++++++++++--------------------
 1 file changed, 449 insertions(+), 449 deletions(-)

diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index e170ec1414..dc6a70bc24 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -7,10 +7,10 @@
     "Error": "Erreur",
     "Existing Call": "Appel en cours",
     "Export E2E room keys": "Exporter les clés de chiffrement de salon",
-    "Failed to ban user": "Échec du bannissement de l'utilisateur",
-    "Failed to change password. Is your password correct?": "Échec du changement de mot de passe. Votre mot de passe est-il correct ?",
+    "Failed to ban user": "Échec du bannissement de l’utilisateur",
+    "Failed to change password. Is your password correct?": "Échec du changement de mot de passe. Votre mot de passe est-il correct ?",
     "Failed to change power level": "Échec du changement de rang",
-    "Failed to forget room %(errCode)s": "Échec de l'oubli du salon %(errCode)s",
+    "Failed to forget room %(errCode)s": "Échec de l’oubli du salon %(errCode)s",
     "Remove": "Supprimer",
     "Favourite": "Favoris",
     "Notifications": "Notifications",
@@ -20,13 +20,13 @@
     "Admin": "Administrateur",
     "Advanced": "Avancé",
     "%(items)s and %(lastItem)s": "%(items)s et %(lastItem)s",
-    "and %(count)s others...|other": "et %(count)s autres...",
-    "and %(count)s others...|one": "et un autre...",
+    "and %(count)s others...|other": "et %(count)s autres…",
+    "and %(count)s others...|one": "et un autre…",
     "A new password must be entered.": "Un nouveau mot de passe doit être saisi.",
     "Anyone who knows the room's link, apart from guests": "Tous ceux qui connaissent le lien du salon, à part les visiteurs",
     "Anyone who knows the room's link, including guests": "Tous ceux qui connaissent le lien du salon, y compris les visiteurs",
-    "Are you sure?": "Êtes-vous sûr(e) ?",
-    "Are you sure you want to reject the invitation?": "Voulez-vous vraiment rejeter l'invitation ?",
+    "Are you sure?": "Êtes-vous sûr ?",
+    "Are you sure you want to reject the invitation?": "Voulez-vous vraiment rejeter l’invitation ?",
     "Attachment": "Pièce jointe",
     "Autoplay GIFs and videos": "Jouer automatiquement les GIFs et les vidéos",
     "%(senderName)s banned %(targetName)s.": "%(senderName)s a banni %(targetName)s.",
@@ -34,13 +34,13 @@
     "Banned users": "Utilisateurs bannis",
     "Bans user with given id": "Bannit l’utilisateur à partir de son identifiant",
     "Call Timeout": "L’appel a dépassé le délai d'attente maximal",
-    "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>.": "Impossible de se connecter au serveur d'accueil en HTTP si l'URL dans la barre de votre explorateur est en HTTPS. Utilisez HTTPS ou <a>activez le support des scripts non-vérifiés</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>.": "Impossible de se connecter au serveur d'accueil en HTTP si l’URL dans la barre de votre explorateur est en HTTPS. Utilisez HTTPS ou <a>activez la prise en charge des scripts non-vérifiés</a>.",
     "Change Password": "Changer le mot de passe",
     "%(senderName)s changed their profile picture.": "%(senderName)s a changé son image de profil.",
     "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s a changé le rang de %(powerLevelDiffText)s.",
     "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s a changé le nom du salon en %(roomName)s.",
     "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s a changé le sujet du salon en « %(topic)s ».",
-    "Changes your display nickname": "Change votre nom affiché",
+    "Changes your display nickname": "Change votre nom d’affichage",
     "Click here to fix": "Cliquer ici pour réparer",
     "Click to mute audio": "Cliquer pour couper le son",
     "Click to mute video": "Cliquer ici pour couper la vidéo",
@@ -59,24 +59,24 @@
     "Decrypt %(text)s": "Déchiffrer %(text)s",
     "Deops user with given id": "Retire le rang d’opérateur d’un utilisateur à partir de son identifiant",
     "Failed to join room": "Échec de l’inscription au salon",
-    "Failed to kick": "Échec de l'expulsion",
+    "Failed to kick": "Échec de l’expulsion",
     "Failed to leave room": "Échec du départ du salon",
-    "Failed to load timeline position": "Échec du chargement de la position dans l'historique",
-    "Failed to mute user": "Échec de la mise en sourdine de l'utilisateur",
-    "Failed to reject invite": "Échec du rejet de l'invitation",
-    "Failed to reject invitation": "Échec du rejet de l'invitation",
+    "Failed to load timeline position": "Échec du chargement de la position dans le fil de discussion",
+    "Failed to mute user": "Échec de la mise en sourdine de l’utilisateur",
+    "Failed to reject invite": "Échec du rejet de l’invitation",
+    "Failed to reject invitation": "Échec du rejet de l’invitation",
     "Failed to send email": "Échec de l’envoi de l’e-mail",
     "Failed to send request.": "Échec de l’envoi de la requête.",
-    "Failed to set display name": "Échec de l'enregistrement du nom affiché",
+    "Failed to set display name": "Échec de l’enregistrement du nom d’affichage",
     "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s a accepté l’invitation de %(displayName)s.",
-    "Access Token:": "Jeton d’accès :",
-    "Always show message timestamps": "Toujours afficher l'heure des messages",
+    "Access Token:": "Jeton d’accès :",
+    "Always show message timestamps": "Toujours afficher l’heure des messages",
     "Authentication": "Authentification",
     "%(senderName)s answered the call.": "%(senderName)s a répondu à l’appel.",
     "An error has occurred.": "Une erreur est survenue.",
     "Email": "E-mail",
     "Failed to unban": "Échec de la révocation du bannissement",
-    "Failed to verify email address: make sure you clicked the link in the email": "La vérification de l’adresse e-mail a échoué : vérifiez que vous avez bien cliqué sur le lien dans l’e-mail",
+    "Failed to verify email address: make sure you clicked the link in the email": "La vérification de l’adresse e-mail a échoué : vérifiez que vous avez bien cliqué sur le lien dans l’e-mail",
     "Failure to create room": "Échec de création du salon",
     "Favourites": "Favoris",
     "Fill screen": "Plein écran",
@@ -86,8 +86,8 @@
     "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s de %(fromPowerLevel)s à %(toPowerLevel)s",
     "Hangup": "Raccrocher",
     "Historical": "Historique",
-    "Homeserver is": "Le serveur d'accueil est",
-    "Identity Server is": "Le serveur d'identité est",
+    "Homeserver is": "Le serveur d’accueil est",
+    "Identity Server is": "Le serveur d’identité est",
     "I have verified my email address": "J’ai vérifié mon adresse e-mail",
     "Import E2E room keys": "Importer les clés de chiffrement de bout en bout",
     "Incorrect verification code": "Code de vérification incorrect",
@@ -121,7 +121,7 @@
     "New passwords must match each other.": "Les nouveaux mots de passe doivent être identiques.",
     "not specified": "non spécifié",
     "(not supported by this browser)": "(non pris en charge par ce navigateur)",
-    "<not supported>": "<non supporté>",
+    "<not supported>": "<non pris en charge>",
     "No more results": "Fin des résultats",
     "No results": "Pas de résultat",
     "unknown error code": "code d’erreur inconnu",
@@ -135,41 +135,41 @@
     "Default": "Par défaut",
     "Email address": "Adresse e-mail",
     "Error decrypting attachment": "Erreur lors du déchiffrement de la pièce jointe",
-    "Guests cannot join this room even if explicitly invited.": "Les visiteurs ne peuvent pas rejoindre ce salon, même s'ils ont été explicitement invités.",
+    "Guests cannot join this room even if explicitly invited.": "Les visiteurs ne peuvent pas rejoindre ce salon, même s’ils ont été explicitement invités.",
     "Invalid file%(extra)s": "Fichier %(extra)s non valide",
     "Mute": "Mettre en sourdine",
     "No users have specific privileges in this room": "Aucun utilisateur n’a de privilège spécifique dans ce salon",
-    "olm version:": "version de olm :",
-    "Please check your email and click on the link it contains. Once this is done, click continue.": "Veuillez vérifier vos e-mails et cliquer sur le lien que vous avez reçu. Puis cliquez sur continuer.",
+    "olm version:": "version de olm :",
+    "Please check your email and click on the link it contains. Once this is done, click continue.": "Veuillez consulter vos e-mails et cliquer sur le lien que vous avez reçu. Puis cliquez sur continuer.",
     "Power level must be positive integer.": "Le rang doit être un entier positif.",
     "Privileged Users": "Utilisateurs privilégiés",
     "Profile": "Profil",
     "Reason": "Raison",
     "%(targetName)s rejected the invitation.": "%(targetName)s a rejeté l’invitation.",
-    "Reject invitation": "Rejeter l'invitation",
-    "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s a supprimé son nom affiché (%(oldDisplayName)s).",
+    "Reject invitation": "Rejeter l’invitation",
+    "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s a supprimé son nom d’affichage (%(oldDisplayName)s).",
     "%(senderName)s removed their profile picture.": "%(senderName)s a supprimé son image de profil.",
     "%(senderName)s requested a VoIP conference.": "%(senderName)s a demandé une téléconférence audio.",
     "Return to login screen": "Retourner à l’écran de connexion",
     "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s n’a pas l’autorisation de vous envoyer des notifications - merci de vérifier les paramètres de votre navigateur",
     "%(brand)s was not given permission to send notifications - please try again": "%(brand)s n’a pas reçu l’autorisation de vous envoyer des notifications - veuillez réessayer",
-    "%(brand)s version:": "Version de %(brand)s :",
+    "%(brand)s version:": "Version de %(brand)s :",
     "Room %(roomId)s not visible": "Le salon %(roomId)s n’est pas visible",
     "Room Colour": "Couleur du salon",
     "Rooms": "Salons",
     "Search": "Rechercher",
     "Search failed": "Échec de la recherche",
     "Searches DuckDuckGo for results": "Recherche des résultats dans DuckDuckGo",
-    "Send Reset Email": "Envoyer l'e-mail de réinitialisation",
+    "Send Reset Email": "Envoyer l’e-mail de réinitialisation",
     "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s a envoyé une image.",
     "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s a invité %(targetDisplayName)s à rejoindre le salon.",
     "Server error": "Erreur du serveur",
     "Server may be unavailable, overloaded, or search timed out :(": "Le serveur semble être inaccessible, surchargé ou la recherche a expiré :(",
     "Server may be unavailable, overloaded, or you hit a bug.": "Le serveur semble être indisponible, surchargé ou vous êtes tombé sur un bug.",
-    "Server unavailable, overloaded, or something else went wrong.": "Le serveur semble être inaccessible, surchargé ou quelque chose s'est mal passé.",
+    "Server unavailable, overloaded, or something else went wrong.": "Le serveur semble être inaccessible, surchargé ou quelque chose s’est mal passé.",
     "Session ID": "Identifiant de session",
     "%(senderName)s set a profile picture.": "%(senderName)s a défini une image de profil.",
-    "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s a défini son nom affiché comme %(displayName)s.",
+    "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s a défini son nom d’affichage comme %(displayName)s.",
     "Show timestamps in 12 hour format (e.g. 2:30pm)": "Afficher l’heure au format am/pm (par ex. 2:30pm)",
     "Signed Out": "Déconnecté",
     "Sign in": "Se connecter",
@@ -182,15 +182,15 @@
     "This email address was not found": "Cette adresse e-mail n’a pas été trouvée",
     "The email address linked to your account must be entered.": "L’adresse e-mail liée à votre compte doit être renseignée.",
     "The remote side failed to pick up": "Le correspondant n’a pas décroché",
-    "This room has no local addresses": "Ce salon n'a pas d'adresse locale",
+    "This room has no local addresses": "Ce salon n’a pas d’adresse locale",
     "This room is not recognised.": "Ce salon n’est pas reconnu.",
     "This doesn't appear to be a valid email address": "Cette adresse e-mail ne semble pas valide",
     "This phone number is already in use": "Ce numéro de téléphone est déjà utilisé",
     "This room is not accessible by remote Matrix servers": "Ce salon n’est pas accessible par les serveurs Matrix distants",
     "To use it, just wait for autocomplete results to load and tab through them.": "Pour l’utiliser, attendez simplement que les résultats de l’auto-complétion s’affichent et défilez avec la touche Tab.",
-    "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Un instant donné de la chronologie n’a pu être chargé car vous n’avez pas la permission de le visualiser.",
-    "Tried to load a specific point in this room's timeline, but was unable to find it.": "Un instant donné de la chronologie n’a pu être chargé car il n’a pas pu être trouvé.",
-    "Unable to add email address": "Impossible d'ajouter l'adresse e-mail",
+    "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Un instant donné du fil de discussion n’a pu être chargé car vous n’avez pas la permission de le visualiser.",
+    "Tried to load a specific point in this room's timeline, but was unable to find it.": "Un instant donné du fil de discussion n’a pu être chargé car il n’a pas pu être trouvé.",
+    "Unable to add email address": "Impossible d'ajouter l’adresse e-mail",
     "Unable to remove contact information": "Impossible de supprimer les informations du contact",
     "Unable to verify email address.": "Impossible de vérifier l’adresse e-mail.",
     "Unban": "Révoquer le bannissement",
@@ -198,31 +198,31 @@
     "Unable to capture screen": "Impossible de faire une capture d’écran",
     "Unable to enable Notifications": "Impossible d’activer les notifications",
     "Unmute": "Activer le son",
-    "Upload avatar": "Télécharger une photo de profil",
+    "Upload avatar": "Envoyer un avatar",
     "Upload Failed": "Échec de l’envoi",
     "Upload file": "Envoyer un fichier",
     "Usage": "Utilisation",
     "Users": "Utilisateurs",
     "Verification Pending": "Vérification en attente",
     "Video call": "Appel vidéo",
-    "Voice call": "Appel vocal",
+    "Voice call": "Appel audio",
     "VoIP conference finished.": "Téléconférence VoIP terminée.",
     "VoIP conference started.": "Téléconférence VoIP démarrée.",
     "VoIP is unsupported": "Voix sur IP non prise en charge",
-    "Warning!": "Attention !",
-    "Who can access this room?": "Qui peut accéder au salon ?",
-    "Who can read history?": "Qui peut lire l'historique ?",
-    "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s a annulé l’invitation de %(targetName)s.",
+    "Warning!": "Attention !",
+    "Who can access this room?": "Qui peut accéder au salon ?",
+    "Who can read history?": "Qui peut lire l’historique ?",
+    "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s a révoqué l’invitation de %(targetName)s.",
     "You are already in a call.": "Vous avez déjà un appel en cours.",
     "You cannot place a call with yourself.": "Vous ne pouvez pas passer d’appel avec vous-même.",
-    "You cannot place VoIP calls in this browser.": "Vous ne pouvez pas passer d’appel en Voix sur IP dans ce navigateur.",
+    "You cannot place VoIP calls in this browser.": "Vous ne pouvez pas passer d’appel en VoIP dans ce navigateur.",
     "You do not have permission to post to this room": "Vous n’avez pas la permission de poster dans ce salon",
     "You have no visible notifications": "Vous n'avez pas de notification visible",
     "You need to be able to invite users to do that.": "Vous devez avoir l’autorisation d’inviter des utilisateurs pour faire ceci.",
     "You need to be logged in.": "Vous devez être identifié.",
     "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Votre adresse e-mail ne semble pas être associée à un identifiant Matrix sur ce serveur d’accueil.",
-    "You seem to be in a call, are you sure you want to quit?": "Vous semblez avoir un appel en cours, voulez-vous vraiment partir ?",
-    "You seem to be uploading files, are you sure you want to quit?": "Vous semblez être en train d'envoyer des fichiers, voulez-vous vraiment partir ?",
+    "You seem to be in a call, are you sure you want to quit?": "Vous semblez avoir un appel en cours, voulez-vous vraiment partir ?",
+    "You seem to be uploading files, are you sure you want to quit?": "Vous semblez être en train d’envoyer des fichiers, voulez-vous vraiment partir ?",
     "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Vous ne pourrez pas annuler cette modification car vous promouvez l’utilisateur au même rang que le vôtre.",
     "Sun": "Dim",
     "Mon": "Lun",
@@ -252,7 +252,7 @@
     "An error occurred: %(error_string)s": "Une erreur est survenue : %(error_string)s",
     "There are no visible files in this room": "Il n'y a pas de fichier visible dans ce salon",
     "Room": "Salon",
-    "Connectivity to the server has been lost.": "La connectivité au serveur a été perdue.",
+    "Connectivity to the server has been lost.": "La connexion au serveur a été perdue.",
     "Sent messages will be stored until your connection has returned.": "Les messages envoyés seront stockés jusqu’à ce que votre connexion revienne.",
     "Cancel": "Annuler",
     "Active call": "Appel en cours",
@@ -260,7 +260,7 @@
     "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s a supprimé le nom du salon.",
     "Analytics": "Collecte de données",
     "%(brand)s collects anonymous analytics to allow us to improve the application.": "%(brand)s collecte des données anonymes qui nous permettent d’améliorer l’application.",
-    "Passphrases must match": "Les phrases de passe doivent être identiques",
+    "Passphrases must match": "Les phrases secrètes doivent être identiques",
     "Passphrase must not be empty": "Le mot de passe ne peut pas être vide",
     "Export room keys": "Exporter les clés de salon",
     "Enter passphrase": "Saisir le mot de passe",
@@ -276,7 +276,7 @@
     "Failed to invite": "Échec de l’invitation",
     "Failed to invite the following users to the %(roomName)s room:": "Échec de l’invitation des utilisateurs suivants dans le salon %(roomName)s :",
     "Confirm Removal": "Confirmer la suppression",
-    "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.": "Voulez-vous vraiment supprimer cet événement ? Notez que si vous supprimez le changement du nom ou du sujet d’un salon, il est possible que ce changement soit annulé.",
+    "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.": "Voulez-vous vraiment supprimer cet événement ? Notez que si vous supprimez le changement du nom ou du sujet d’un salon, il est possible que ce changement soit annulé.",
     "Unknown error": "Erreur inconnue",
     "Incorrect password": "Mot de passe incorrect",
     "Unable to restore session": "Impossible de restaurer la session",
@@ -288,7 +288,7 @@
     "Dismiss": "Ignorer",
     "Please check your email to continue registration.": "Merci de vérifier votre e-mail afin de continuer votre inscription.",
     "Token incorrect": "Jeton incorrect",
-    "Please enter the code it contains:": "Merci de saisir le code qu'il contient :",
+    "Please enter the code it contains:": "Merci de saisir le code qu’il contient :",
     "powered by Matrix": "propulsé par Matrix",
     "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Si vous ne renseignez pas d’adresse e-mail, vous ne pourrez pas réinitialiser votre mot de passe. En êtes vous sûr(e) ?",
     "Error decrypting audio": "Erreur lors du déchiffrement de l’audio",
@@ -296,7 +296,7 @@
     "Error decrypting video": "Erreur lors du déchiffrement de la vidéo",
     "Add an Integration": "Ajouter une intégration",
     "URL Previews": "Aperçus des liens",
-    "Drop file here to upload": "Déposer le fichier ici pour l'envoyer",
+    "Drop file here to upload": "Glisser le fichier ici pour l’envoyer",
     " (unsupported)": " (pas pris en charge)",
     "Ongoing conference call%(supportedText)s.": "Téléconférence en cours%(supportedText)s.",
     "Online": "En ligne",
@@ -305,7 +305,7 @@
     "Idle": "Inactif",
     "Jump to first unread message.": "Aller au premier message non lu.",
     "Options": "Options",
-    "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?": "Vous êtes sur le point d’accéder à un site tiers afin de pouvoir vous identifier pour utiliser %(integrationsUrl)s. Voulez-vous continuer ?",
+    "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?": "Vous êtes sur le point d’accéder à un site tiers afin de pouvoir vous identifier pour utiliser %(integrationsUrl)s. Voulez-vous continuer ?",
     "%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s a changé l’avatar du salon en <img/>",
     "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s a supprimé l'avatar du salon.",
     "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s a changé l’avatar de %(roomName)s",
@@ -315,17 +315,17 @@
     "Results from DuckDuckGo": "Résultats de DuckDuckGo",
     "Verified key": "Clé vérifiée",
     "No Microphones detected": "Aucun micro détecté",
-    "No Webcams detected": "Aucune webcam détectée",
+    "No Webcams detected": "Aucune caméra détectée",
     "No media permissions": "Pas de permission pour les médias",
-    "You may need to manually permit %(brand)s to access your microphone/webcam": "Il est possible que vous deviez manuellement autoriser %(brand)s à accéder à votre micro/webcam",
+    "You may need to manually permit %(brand)s to access your microphone/webcam": "Il est possible que vous deviez manuellement autoriser %(brand)s à accéder à votre micro/caméra",
     "Default Device": "Appareil par défaut",
     "Microphone": "Micro",
     "Camera": "Caméra",
     "Add a topic": "Ajouter un sujet",
-    "Anyone": "N'importe qui",
-    "Are you sure you want to leave the room '%(roomName)s'?": "Voulez-vous vraiment quitter le salon \"%(roomName)s\" ?",
+    "Anyone": "N’importe qui",
+    "Are you sure you want to leave the room '%(roomName)s'?": "Voulez-vous vraiment quitter le salon « %(roomName)s » ?",
     "Custom level": "Rang personnalisé",
-    "Register": "S'inscrire",
+    "Register": "S’inscrire",
     "Save": "Enregistrer",
     "You have <a>disabled</a> URL previews by default.": "Vous avez <a>désactivé</a> les aperçus d’URL par défaut.",
     "You have <a>enabled</a> URL previews by default.": "Vous avez <a>activé</a> les aperçus d’URL par défaut.",
@@ -339,11 +339,11 @@
     "You must <a>register</a> to use this functionality": "Vous devez vous <a>inscrire</a> pour utiliser cette fonctionnalité",
     "Create new room": "Créer un nouveau salon",
     "Room directory": "Répertoire des salons",
-    "Start chat": "Commencer une discussion",
+    "Start chat": "Commencer un conversation privée",
     "New Password": "Nouveau mot de passe",
     "Username available": "Nom d'utilisateur disponible",
     "Username not available": "Nom d'utilisateur indisponible",
-    "Something went wrong!": "Quelque chose s’est mal passé !",
+    "Something went wrong!": "Quelque chose s’est mal déroulé !",
     "This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Cela sera le nom de votre compte sur le serveur d'accueil  <span></span>, ou vous pouvez sélectionner un <a>autre serveur</a>.",
     "If you already have a Matrix account you can <a>log in</a> instead.": "Si vous avez déjà un compte Matrix vous pouvez vous <a>connecter</a> à la place.",
     "Accept": "Accepter",
@@ -352,18 +352,18 @@
     "Close": "Fermer",
     "Custom": "Personnaliser",
     "Decline": "Refuser",
-    "Drop File Here": "Déposer le fichier Ici",
-    "Failed to upload profile picture!": "Échec de l'envoi de l'image de profil !",
+    "Drop File Here": "Glisser le fichier ici",
+    "Failed to upload profile picture!": "Échec de l’envoi de l’image de profil !",
     "Incoming call from %(name)s": "Appel entrant de %(name)s",
     "Incoming video call from %(name)s": "Appel vidéo entrant de %(name)s",
     "Incoming voice call from %(name)s": "Appel vocal entrant de %(name)s",
-    "No display name": "Pas de nom affiché",
+    "No display name": "Pas de nom d’affichage",
     "Private Chat": "Discussion privée",
     "Public Chat": "Discussion publique",
-    "%(roomName)s does not exist.": "%(roomName)s n'existe pas.",
-    "%(roomName)s is not accessible at this time.": "%(roomName)s n'est pas accessible pour le moment.",
+    "%(roomName)s does not exist.": "%(roomName)s n’existe pas.",
+    "%(roomName)s is not accessible at this time.": "%(roomName)s n’est pas joignable pour le moment.",
     "Seen by %(userName)s at %(dateTime)s": "Vu par %(userName)s à %(dateTime)s",
-    "Start authentication": "Commencer une authentification",
+    "Start authentication": "Commencer l’authentification",
     "This room": "Ce salon",
     "unknown caller": "appelant inconnu",
     "Unnamed Room": "Salon anonyme",
@@ -371,17 +371,17 @@
     "(~%(count)s results)|one": "(~%(count)s résultat)",
     "(~%(count)s results)|other": "(~%(count)s résultats)",
     "Home": "Accueil",
-    "Upload new:": "Envoyer un nouveau :",
+    "Upload new:": "Envoyer un nouveau :",
     "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Rejoindre en <voiceText>audio</voiceText> ou en <videoText>vidéo</videoText>.",
     "Last seen": "Vu pour la dernière fois",
     "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (rang %(powerLevelNumber)s)",
     "(could not connect media)": "(impossible de se connecter au média)",
     "(no answer)": "(pas de réponse)",
     "(unknown failure: %(reason)s)": "(erreur inconnue : %(reason)s)",
-    "Your browser does not support the required cryptography extensions": "Votre navigateur ne supporte pas les extensions cryptographiques nécessaires",
+    "Your browser does not support the required cryptography extensions": "Votre navigateur ne prend pas en charge les extensions cryptographiques nécessaires",
     "Not a valid %(brand)s keyfile": "Fichier de clé %(brand)s non valide",
-    "Authentication check failed: incorrect password?": "Erreur d’authentification : mot de passe incorrect ?",
-    "Do you want to set an email address?": "Souhaitez-vous configurer une adresse e-mail ?",
+    "Authentication check failed: incorrect password?": "Erreur d’authentification : mot de passe incorrect ?",
+    "Do you want to set an email address?": "Souhaitez-vous configurer une adresse e-mail ?",
     "This will allow you to reset your password and receive notifications.": "Ceci vous permettra de réinitialiser votre mot de passe et de recevoir des notifications.",
     "Skip": "Passer",
     "Check for update": "Rechercher une mise à jour",
@@ -397,13 +397,13 @@
     "You do not have permission to do that in this room.": "Vous n’avez pas l’autorisation d’effectuer cette action dans ce salon.",
     "Example": "Exemple",
     "Create": "Créer",
-    "Featured Rooms:": "Salons mis en avant :",
-    "Featured Users:": "Utilisateurs mis en avant :",
+    "Featured Rooms:": "Salons mis en avant :",
+    "Featured Users:": "Utilisateurs mis en avant :",
     "Automatically replace plain text Emoji": "Remplacer automatiquement le texte par des émojis",
-    "Failed to upload image": "Impossible d'envoyer l'image",
+    "Failed to upload image": "Impossible d’envoyer l’image",
     "%(widgetName)s widget added by %(senderName)s": "Widget %(widgetName)s ajouté par %(senderName)s",
     "%(widgetName)s widget removed by %(senderName)s": "Widget %(widgetName)s supprimé par %(senderName)s",
-    "Publish this room to the public in %(domain)s's room directory?": "Publier ce salon dans le répertoire de salons public de %(domain)s ?",
+    "Publish this room to the public in %(domain)s's room directory?": "Publier ce salon dans le répertoire de salons public de %(domain)s ?",
     "Cannot add any more widgets": "Impossible d'ajouter plus de widgets",
     "The maximum permitted number of widgets have already been added to this room.": "Le nombre maximum de widgets autorisés a déjà été atteint pour ce salon.",
     "AM": "AM",
@@ -432,26 +432,26 @@
     "Unignore": "Ne plus ignorer",
     "Ignore": "Ignorer",
     "Invite": "Inviter",
-    "Admin Tools": "Outils d'administration",
+    "Admin Tools": "Outils d’administration",
     "Unpin Message": "Dépingler le message",
     "Jump to message": "Aller au message",
     "No pinned messages.": "Aucun message épinglé.",
-    "Loading...": "Chargement...",
+    "Loading...": "Chargement…",
     "Pinned Messages": "Messages épinglés",
     "Unknown": "Inconnu",
     "Unnamed room": "Salon sans nom",
     "No rooms to show": "Aucun salon à afficher",
     "Banned by %(displayName)s": "Banni par %(displayName)s",
     "%(senderName)s changed the pinned messages for the room.": "%(senderName)s a changé les messages épinglés du salon.",
-    "Jump to read receipt": "Aller à l'accusé de lecture",
+    "Jump to read receipt": "Aller à l’accusé de lecture",
     "World readable": "Lisible publiquement",
     "Guests can join": "Accessible aux visiteurs",
     "Invalid community ID": "Identifiant de communauté non valide",
-    "'%(groupId)s' is not a valid community ID": "\"%(groupId)s\" n'est pas un identifiant de communauté valide",
+    "'%(groupId)s' is not a valid community ID": "« %(groupId)s » n’est pas un identifiant de communauté valide",
     "%(senderName)s sent an image": "%(senderName)s a envoyé une image",
     "%(senderName)s sent a video": "%(senderName)s a envoyé une vidéo",
     "%(senderName)s uploaded a file": "%(senderName)s a transféré un fichier",
-    "Disinvite this user?": "Désinviter l'utilisateur ?",
+    "Disinvite this user?": "Désinviter l’utilisateur ?",
     "Kick this user?": "Expulser cet utilisateur ?",
     "Unban this user?": "Révoquer le bannissement de cet utilisateur ?",
     "Ban this user?": "Bannir cet utilisateur ?",
@@ -464,13 +464,13 @@
     "Remove from community": "Supprimer de la communauté",
     "Disinvite this user from community?": "Désinviter cet utilisateur de la communauté ?",
     "Remove this user from community?": "Supprimer cet utilisateur de la communauté ?",
-    "Failed to withdraw invitation": "Échec de l'annulation de l'invitation",
-    "Failed to remove user from community": "Échec de la suppression de l'utilisateur de la communauté",
+    "Failed to withdraw invitation": "Échec de l’annulation de l’invitation",
+    "Failed to remove user from community": "Échec de la suppression de l’utilisateur de la communauté",
     "Filter community members": "Filtrer les membres de la communauté",
     "Filter community rooms": "Filtrer les salons de la communauté",
     "Failed to remove room from community": "Échec de la suppression du salon de la communauté",
-    "Failed to remove '%(roomName)s' from %(groupId)s": "Échec de la suppression de \"%(roomName)s\" de %(groupId)s",
-    "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Voulez-vous vraiment supprimer \"%(roomName)s\" de %(groupId)s ?",
+    "Failed to remove '%(roomName)s' from %(groupId)s": "Échec de la suppression de « %(roomName)s » de %(groupId)s",
+    "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Voulez-vous vraiment supprimer « %(roomName)s » de %(groupId)s ?",
     "Removing a room from the community will also remove it from the community page.": "Supprimer un salon de la communauté le supprimera aussi de la page de la communauté.",
     "Delete Widget": "Supprimer le widget",
     "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Supprimer un widget le supprime pour tous les utilisateurs du salon. Voulez-vous vraiment supprimer ce widget ?",
@@ -525,13 +525,13 @@
     "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s a changé d'avatar",
     "%(items)s and %(count)s others|other": "%(items)s et %(count)s autres",
     "%(items)s and %(count)s others|one": "%(items)s et un autre",
-    "And %(count)s more...|other": "Et %(count)s autres...",
+    "And %(count)s more...|other": "Et %(count)s autres…",
     "Matrix ID": "Identifiant Matrix",
     "Matrix Room ID": "Identifiant de salon Matrix",
     "email address": "adresse e-mail",
-    "Try using one of the following valid address types: %(validTypesList)s.": "Essayez d'utiliser un des types d'adresse valide suivants : %(validTypesList)s.",
-    "You have entered an invalid address.": "L'adresse saisie n'est pas valide.",
-    "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Les identifiants de communauté ne peuvent contenir que les caractères a-z, 0-9 ou '=_-./'",
+    "Try using one of the following valid address types: %(validTypesList)s.": "Essayez d’utiliser un des types d’adresse valide suivants : %(validTypesList)s.",
+    "You have entered an invalid address.": "L’adresse saisie n’est pas valide.",
+    "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Les identifiants de communauté ne peuvent contenir que les caractères a-z, 0-9 ou « =_-./ »",
     "Something went wrong whilst creating your community": "Une erreur est survenue lors de la création de votre communauté",
     "Create Community": "Créer une communauté",
     "Community Name": "Nom de la communauté",
@@ -540,41 +540,41 @@
     "Add rooms to the community summary": "Ajouter des salons au sommaire de la communauté",
     "Which rooms would you like to add to this summary?": "Quels salons souhaitez-vous ajouter à ce sommaire ?",
     "Add to summary": "Ajouter au sommaire",
-    "Failed to add the following rooms to the summary of %(groupId)s:": "Échec de l'ajout des salons suivants au sommaire de %(groupId)s :",
+    "Failed to add the following rooms to the summary of %(groupId)s:": "Échec de l’ajout des salons suivants au sommaire de %(groupId)s :",
     "Add a Room": "Ajouter un salon",
     "Failed to remove the room from the summary of %(groupId)s": "Échec de la suppression du salon du sommaire de %(groupId)s",
-    "The room '%(roomName)s' could not be removed from the summary.": "Le salon \"%(roomName)s\" n'a pas pu être supprimé du sommaire.",
+    "The room '%(roomName)s' could not be removed from the summary.": "Le salon « %(roomName)s » n’a pas pu être supprimé du sommaire.",
     "Add users to the community summary": "Ajouter des utilisateurs au sommaire de la communauté",
     "Who would you like to add to this summary?": "Qui souhaitez-vous ajouter à ce sommaire ?",
-    "Failed to add the following users to the summary of %(groupId)s:": "Échec de l'ajout des utilisateurs suivants au sommaire de %(groupId)s :",
+    "Failed to add the following users to the summary of %(groupId)s:": "Échec de l’ajout des utilisateurs suivants au sommaire de %(groupId)s :",
     "Add a User": "Ajouter un utilisateur",
-    "Failed to remove a user from the summary of %(groupId)s": "Échec de la suppression d'un utilisateur du sommaire de %(groupId)s",
-    "The user '%(displayName)s' could not be removed from the summary.": "L'utilisateur \"%(displayName)s\" n'a pas pu être supprimé du sommaire.",
+    "Failed to remove a user from the summary of %(groupId)s": "Échec de la suppression d’un utilisateur du sommaire de %(groupId)s",
+    "The user '%(displayName)s' could not be removed from the summary.": "L'utilisateur « %(displayName)s » n'a pas pu être supprimé du sommaire.",
     "Failed to update community": "Échec de la mise à jour de la communauté",
-    "Unable to accept invite": "Impossible d'accepter l'invitation",
-    "Unable to reject invite": "Impossible de décliner l'invitation",
+    "Unable to accept invite": "Impossible d’accepter l’invitation",
+    "Unable to reject invite": "Impossible de décliner l’invitation",
     "Leave Community": "Quitter la communauté",
     "Leave %(groupName)s?": "Quitter %(groupName)s ?",
     "Leave": "Quitter",
     "Community Settings": "Paramètres de la communauté",
     "Add rooms to this community": "Ajouter des salons à cette communauté",
     "%(inviter)s has invited you to join this community": "%(inviter)s vous a invité à rejoindre cette communauté",
-    "You are an administrator of this community": "Vous êtes un(e) administrateur(trice) de cette communauté",
+    "You are an administrator of this community": "Vous êtes un administrateur de cette communauté",
     "You are a member of this community": "Vous êtes un membre de cette communauté",
     "Long Description (HTML)": "Description longue (HTML)",
     "Description": "Description",
     "Community %(groupId)s not found": "Communauté %(groupId)s non trouvée",
     "Failed to load %(groupId)s": "Échec du chargement de %(groupId)s",
     "Your Communities": "Vos communautés",
-    "You're not currently a member of any communities.": "Vous n'êtes actuellement membre d'aucune communauté.",
-    "Error whilst fetching joined communities": "Erreur lors de l'obtention des communautés rejointes",
+    "You're not currently a member of any communities.": "Vous n’êtes actuellement membre d’aucune communauté.",
+    "Error whilst fetching joined communities": "Erreur lors de la récupération des communautés dont vous faites partie",
     "Create a new community": "Créer une nouvelle communauté",
-    "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Créez une communauté pour grouper des utilisateurs et des salons ! Construisez une page d'accueil personnalisée pour distinguer votre espace dans l'univers Matrix.",
+    "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Créez une communauté pour grouper des utilisateurs et des salons ! Construisez une page d’accueil personnalisée pour distinguer votre espace dans l’univers Matrix.",
     "Mirror local video feed": "Inverser horizontalement la vidéo locale (effet miroir)",
     "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "Un e-mail a été envoyé à %(emailAddress)s. Après avoir suivi le lien présent dans celui-ci, cliquez ci-dessous.",
     "Ignores a user, hiding their messages from you": "Ignore un utilisateur, en masquant ses messages",
     "Stops ignoring a user, showing their messages going forward": "Arrête d’ignorer un utilisateur, en affichant ses messages à partir de maintenant",
-    "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "La visibilité de \"%(roomName)s\" dans %(groupId)s n'a pas pu être mise à jour.",
+    "The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "La visibilité de « %(roomName)s » dans %(groupId)s n’a pas pu être mise à jour.",
     "Visibility in Room List": "Visibilité dans la liste des salons",
     "Visible to everyone": "Visible pour tout le monde",
     "Only visible to community members": "Visible uniquement par les membres de la communauté",
@@ -583,13 +583,13 @@
     "Room Notification": "Notification du salon",
     "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Ces salons sont affichés aux membres de la communauté sur la page de la communauté. Les membres de la communauté peuvent rejoindre ces salons en cliquant dessus.",
     "<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 use 'img' tags\n</p>\n": "<h1>HTML pour votre page de communauté</h1>\n<p>\n    Utilisez la description longue pour présenter la communauté aux nouveaux membres\n    ou pour diffuser des <a href=\"foo\">liens</a> importants\n</p>\n<p>\n    Vous pouvez même utiliser des balises \"img\"\n</p>\n",
-    "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!": "Votre communauté n'a pas de description longue, une page HTML à montrer aux membres de la communauté.<br />Cliquez ici pour ouvrir les réglages et créez-la !",
+    "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!": "Votre communauté n’a pas de description longue, une page HTML à montrer aux membres de la communauté.<br />Cliquez ici pour ouvrir les réglages et créez-la !",
     "Show these rooms to non-members on the community page and room list?": "Afficher ces salons aux non-membres sur la page de communauté et la liste des salons ?",
     "Please note you are logging into the %(hs)s server, not matrix.org.": "Veuillez noter que vous vous connectez au serveur %(hs)s, pas à matrix.org.",
     "Restricted": "Restreint",
-    "Enable inline URL previews by default": "Activer l'aperçu des URL par défaut",
-    "Enable URL previews for this room (only affects you)": "Activer l'aperçu des URL pour ce salon (n'affecte que vous)",
-    "Enable URL previews by default for participants in this room": "Activer l'aperçu des URL par défaut pour les participants de ce salon",
+    "Enable inline URL previews by default": "Activer l’aperçu des URL par défaut",
+    "Enable URL previews for this room (only affects you)": "Activer l’aperçu des URL pour ce salon (n’affecte que vous)",
+    "Enable URL previews by default for participants in this room": "Activer l’aperçu des URL par défaut pour les participants de ce salon",
     "URL previews are enabled by default for participants in this room.": "Les aperçus d'URL sont activés par défaut pour les participants de ce salon.",
     "URL previews are disabled by default for participants in this room.": "Les aperçus d'URL sont désactivés par défaut pour les participants de ce salon.",
     "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "Il n'y a personne d'autre ici ! Souhaitez-vous <inviteText>inviter d'autres personnes</inviteText> ou <nowarnText>ne plus être notifié à propos du salon vide</nowarnText> ?",
@@ -601,22 +601,22 @@
     "Idle for %(duration)s": "Inactif depuis %(duration)s",
     "Offline for %(duration)s": "Hors ligne depuis %(duration)s",
     "Unknown for %(duration)s": "Inconnu depuis %(duration)s",
-    "Something went wrong when trying to get your communities.": "Une erreur est survenue lors de l'obtention de vos communautés.",
-    "This homeserver doesn't offer any login flows which are supported by this client.": "Ce serveur d'accueil n'offre aucune méthode d'identification compatible avec ce client.",
+    "Something went wrong when trying to get your communities.": "Une erreur est survenue lors de la récupération de vos communautés.",
+    "This homeserver doesn't offer any login flows which are supported by this client.": "Ce serveur d’accueil n’offre aucune méthode d’identification compatible avec ce client.",
     "Flair": "Badge",
-    "Showing flair for these communities:": "Ce salon affichera les badges pour ces communautés :",
-    "This room is not showing flair for any communities": "Ce salon n'affiche de badge pour aucune communauté",
+    "Showing flair for these communities:": "Ce salon affichera les badges pour ces communautés :",
+    "This room is not showing flair for any communities": "Ce salon n’affiche de badge pour aucune communauté",
     "Display your community flair in rooms configured to show it.": "Sélectionnez les badges dans les paramètres de chaque salon pour les afficher.",
     "expand": "développer",
     "collapse": "réduire",
     "Call Failed": "L’appel a échoué",
     "Send": "Envoyer",
     "Old cryptography data detected": "Anciennes données de chiffrement détectées",
-    "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Nous avons détecté des données d'une ancienne version de %(brand)s. Le chiffrement de bout en bout n'aura pas fonctionné correctement sur l'ancienne version. Les messages chiffrés échangés récemment dans l'ancienne version ne sont peut-être pas déchiffrables dans cette version. Les échanges de message avec cette version peuvent aussi échouer. Si vous rencontrez des problèmes, déconnectez-vous puis reconnectez-vous. Pour conserver l'historique des messages, exportez puis réimportez vos clés de chiffrement.",
+    "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Nous avons détecté des données d’une ancienne version de %(brand)s. Le chiffrement de bout en bout n’aura pas fonctionné correctement sur l’ancienne version. Les messages chiffrés échangés récemment dans l’ancienne version ne sont peut-être pas déchiffrables dans cette version. Les échanges de message avec cette version peuvent aussi échouer. Si vous rencontrez des problèmes, déconnectez-vous puis reconnectez-vous. Pour conserver l’historique des messages, exportez puis réimportez vos clés de chiffrement.",
     "Warning": "Attention",
-    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Vous ne pourrez pas annuler cette modification car vous vous destituez. Si vous êtes le dernier utilisateur privilégié de ce salon, il sera impossible de récupérer les privilèges.",
-    "%(count)s of your messages have not been sent.|one": "Votre message n'a pas été envoyé.",
-    "%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Tout renvoyer</resendText> ou <cancelText>tout annuler</cancelText> maintenant. Vous pouvez aussi choisir des messages individuels à renvoyer ou annuler.",
+    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Vous ne pourrez pas annuler cette modification car vous vous rétrogradez. Si vous êtes le dernier utilisateur privilégié de ce salon, il sera impossible de récupérer les privilèges.",
+    "%(count)s of your messages have not been sent.|one": "Votre message n’a pas été envoyé.",
+    "%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Tout renvoyer</resendText> ou <cancelText>tout annuler</cancelText> maintenant. Vous pouvez aussi choisir de ne renvoyer ou annuler que certains messages.",
     "%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Renvoyer le message</resendText> ou <cancelText>annuler le message</cancelText> maintenant.",
     "Send an encrypted reply…": "Envoyer une réponse chiffrée…",
     "Send an encrypted message…": "Envoyer un message chiffré…",
@@ -633,31 +633,31 @@
     "Whether or not you're using the Richtext mode of the Rich Text Editor": "Si vous utilisez le mode « texte enrichi » de l’éditeur de texte enrichi",
     "Your homeserver's URL": "L’URL de votre serveur d’accueil",
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s %(day)s %(monthName)s %(fullYear)s",
-    "This room is not public. You will not be able to rejoin without an invite.": "Ce salon n'est pas public. Vous ne pourrez pas y revenir sans invitation.",
+    "This room is not public. You will not be able to rejoin without an invite.": "Ce salon n’est pas public. Vous ne pourrez pas y revenir sans invitation.",
     "Community IDs cannot be empty.": "Les identifiants de communauté ne peuvent pas être vides.",
     "<a>In reply to</a> <pill>": "<a>En réponse à</a> <pill>",
-    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s a changé son nom affiché en %(displayName)s.",
-    "Failed to set direct chat tag": "Échec de l'ajout de l'étiquette discussion directe",
-    "Failed to remove tag %(tagName)s from room": "Échec de la suppression de l'étiquette %(tagName)s du salon",
-    "Failed to add tag %(tagName)s to room": "Échec de l'ajout de l'étiquette %(tagName)s au salon",
+    "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s a changé son nom d’affichage en %(displayName)s.",
+    "Failed to set direct chat tag": "Échec de l’ajout de l’étiquette discussion directe",
+    "Failed to remove tag %(tagName)s from room": "Échec de la suppression de l’étiquette %(tagName)s du salon",
+    "Failed to add tag %(tagName)s to room": "Échec de l’ajout de l’étiquette %(tagName)s au salon",
     "Clear filter": "Supprimer les filtres",
     "Did you know: you can use communities to filter your %(brand)s experience!": "Le saviez-vous : vous pouvez utiliser les communautés pour filtrer votre expérience %(brand)s !",
-    "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Pour activer un filtre, faites glisser un avatar de communauté sur le panneau des filtres tout à gauche de l'écran. Vous pouvez cliquer sur un avatar dans ce panneau quand vous le souhaitez afin de ne voir que les salons et les personnes associés à cette communauté.",
+    "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Pour activer un filtre, faites glisser un avatar de communauté sur le panneau des filtres tout à gauche de l’écran. Vous pouvez cliquer sur un avatar dans ce panneau quand vous le souhaitez afin de ne voir que les salons et les personnes associés à cette communauté.",
     "Key request sent.": "Demande de clé envoyée.",
     "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Vu par %(displayName)s (%(userName)s) à %(dateTime)s",
     "Code": "Code",
-    "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Si vous avez signalé un bug via GitHub, les journaux de débogage peuvent nous aider à identifier le problème. Les journaux de débogage contiennent des données d'utilisation de l'application dont votre nom d'utilisateur, les identifiants ou alias des salons ou groupes que vous avez visité et les noms d'utilisateur des autres participants. Ils ne contiennent pas les messages.",
+    "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Si vous avez signalé un bug via GitHub, les journaux de débogage peuvent nous aider à identifier le problème. Les journaux de débogage contiennent des données d’utilisation de l’application dont votre nom d’utilisateur, les identifiants ou alias des salons ou groupes que vous avez visité et les noms d’utilisateur des autres participants. Ils ne contiennent pas les messages.",
     "Submit debug logs": "Envoyer les journaux de débogage",
     "Opens the Developer Tools dialog": "Ouvre la fenêtre des outils de développeur",
     "Unable to join community": "Impossible de rejoindre la communauté",
     "Unable to leave community": "Impossible de quitter la communauté",
-    "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Les changements effectués au <bold1>nom</bold1> et à l'<bold2>avatar</bold2> de votre communauté peuvent prendre jusqu'à 30 minutes avant d'être vus par d'autres utilisateurs.",
+    "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Les changements effectués au <bold1>nom</bold1> et à l’<bold2>avatar</bold2> de votre communauté peuvent prendre jusqu'à 30 minutes avant d’être visibles par d’autres utilisateurs.",
     "Join this community": "Rejoindre cette communauté",
     "Leave this community": "Quitter cette communauté",
-    "Stickerpack": "Pack de stickers",
-    "You don't currently have any stickerpacks enabled": "Vous n'avez activé aucun pack de stickers pour l'instant",
-    "Hide Stickers": "Masquer les stickers",
-    "Show Stickers": "Afficher les stickers",
+    "Stickerpack": "Jeu d’autocollants",
+    "You don't currently have any stickerpacks enabled": "Vous n'avez activé aucun jeu d’autocollants pour l’instant",
+    "Hide Stickers": "Masquer les autocollants",
+    "Show Stickers": "Afficher les autocollants",
     "Who can join this community?": "Qui peut rejoindre cette communauté ?",
     "Everyone": "Tout le monde",
     "Fetching third party location failed": "Échec de la récupération de la localisation tierce",
@@ -666,7 +666,7 @@
     "Uploading report": "Envoi du rapport",
     "Sunday": "Dimanche",
     "Notification targets": "Appareils recevant les notifications",
-    "Today": "Aujourd'hui",
+    "Today": "Aujourd’hui",
     "Files": "Fichiers",
     "You are not receiving desktop notifications": "Vous ne recevez pas les notifications sur votre bureau",
     "Friday": "Vendredi",
@@ -675,25 +675,25 @@
     "On": "Activé",
     "Changelog": "Journal des modifications",
     "Waiting for response from server": "En attente d’une réponse du serveur",
-    "Send Custom Event": "Envoyer l'événement personnalisé",
+    "Send Custom Event": "Envoyer l’événement personnalisé",
     "Advanced notification settings": "Paramètres de notification avancés",
     "Forget": "Oublier",
     "You cannot delete this image. (%(code)s)": "Vous ne pouvez pas supprimer cette image. (%(code)s)",
-    "Cancel Sending": "Annuler l'envoi",
+    "Cancel Sending": "Annuler l’envoi",
     "This Room": "Ce salon",
     "Noisy": "Sonore",
     "Room not found": "Salon non trouvé",
-    "Messages containing my display name": "Messages contenant mon nom affiché",
+    "Messages containing my display name": "Messages contenant mon nom d’affichage",
     "Messages in one-to-one chats": "Messages dans les discussions directes",
     "Unavailable": "Indisponible",
     "View Decrypted Source": "Voir la source déchiffrée",
-    "Failed to update keywords": "Échec dans la mise à jour des mots-clés",
+    "Failed to update keywords": "Échec de la mise à jour des mots-clés",
     "remove %(name)s from the directory.": "supprimer %(name)s du répertoire.",
-    "Notifications on the following keywords follow rules which can’t be displayed here:": "Les notifications pour les mots-clés suivant répondent à des critères qui ne peuvent pas être affichés ici :",
+    "Notifications on the following keywords follow rules which can’t be displayed here:": "Les notifications pour les mots-clés suivant répondent à des critères qui ne peuvent pas être affichés ici :",
     "Please set a password!": "Veuillez définir un mot de passe !",
     "You have successfully set a password!": "Vous avez défini un mot de passe avec succès !",
     "An error occurred whilst saving your email notification preferences.": "Une erreur est survenue lors de la sauvegarde de vos préférences de notification par e-mail.",
-    "Explore Room State": "Parcourir l'état du salon",
+    "Explore Room State": "Parcourir l’état du salon",
     "Source URL": "URL de la source",
     "Messages sent by bot": "Messages envoyés par des robots",
     "Filter results": "Filtrer les résultats",
@@ -707,10 +707,10 @@
     "Messages containing <span>keywords</span>": "Messages contenant des <span>mots-clés</span>",
     "Error saving email notification preferences": "Erreur lors de la sauvegarde des préférences de notification par e-mail",
     "Tuesday": "Mardi",
-    "Enter keywords separated by a comma:": "Entrez les mots-clés séparés par une virgule :",
+    "Enter keywords separated by a comma:": "Entrez les mots-clés séparés par une virgule :",
     "Search…": "Rechercher…",
     "You have successfully set a password and an email address!": "Vous avez défini un mot de passe et une adresse e-mail avec succès !",
-    "Remove %(name)s from the directory?": "Supprimer %(name)s du répertoire ?",
+    "Remove %(name)s from the directory?": "Supprimer %(name)s du répertoire ?",
     "%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s utilise de nombreuses fonctionnalités avancées du navigateur, certaines ne sont pas disponibles ou expérimentales dans votre navigateur actuel.",
     "Developer Tools": "Outils de développement",
     "Remember, you can always set an email address in user settings if you change your mind.": "Souvenez-vous que vous pourrez toujours définir une adresse e-mail dans les paramètres de l'utilisateur si vous changez d’avis.",
@@ -727,7 +727,7 @@
     "Enable them now": "Les activer maintenant",
     "Toolbox": "Boîte à outils",
     "Collecting logs": "Récupération des journaux",
-    "You must specify an event type!": "Vous devez spécifier un type d'événement !",
+    "You must specify an event type!": "Vous devez indiquer un type d’événement !",
     "(HTTP status %(httpStatus)s)": "(état HTTP %(httpStatus)s)",
     "Invite to this room": "Inviter dans ce salon",
     "Wednesday": "Mercredi",
@@ -736,15 +736,15 @@
     "Send logs": "Envoyer les journaux",
     "All messages": "Tous les messages",
     "Call invitation": "Appel entrant",
-    "Downloading update...": "Mise à jour en cours de téléchargement...",
-    "State Key": "Clé d'état",
-    "Failed to send custom event.": "Échec de l'envoi de l'événement personnalisé.",
+    "Downloading update...": "Mise à jour en cours de téléchargement…",
+    "State Key": "Clé d’état",
+    "Failed to send custom event.": "Échec de l’envoi de l’événement personnalisé.",
     "What's new?": "Nouveautés",
     "Notify me for anything else": "Me notifier pour tout le reste",
     "View Source": "Voir la source",
-    "Can't update user notification settings": "Impossible de mettre à jour les paramètres de notification de l'utilisateur",
+    "Can't update user notification settings": "Impossible de mettre à jour les paramètres de notification de l’utilisateur",
     "Notify for all other messages/rooms": "Me notifier pour tous les autres messages/salons",
-    "Unable to look up room ID from server": "Impossible de récupérer l'ID du salon sur le serveur",
+    "Unable to look up room ID from server": "Impossible de récupérer l’identifiant du salon sur le serveur",
     "Couldn't find a matching Matrix room": "Impossible de trouver un salon Matrix correspondant",
     "All Rooms": "Tous les salons",
     "Thursday": "Jeudi",
@@ -752,7 +752,7 @@
     "Back": "Retour",
     "Reply": "Répondre",
     "Show message in desktop notification": "Afficher le message dans les notifications de bureau",
-    "Unhide Preview": "Dévoiler l'aperçu",
+    "Unhide Preview": "Dévoiler l’aperçu",
     "Unable to join network": "Impossible de rejoindre le réseau",
     "Sorry, your browser is <b>not</b> able to run %(brand)s.": "Désolé, %(brand)s n'est <b>pas</b> supporté par votre navigateur.",
     "Uploaded on %(date)s by %(user)s": "Téléchargé le %(date)s par %(user)s",
@@ -767,21 +767,21 @@
     "Mentions only": "Seulement les mentions",
     "You can now return to your account after signing out, and sign in on other devices.": "Vous pouvez maintenant revenir sur votre compte après vous être déconnecté, et vous identifier sur d'autres appareils.",
     "Enable email notifications": "Activer les notifications par e-mail",
-    "Event Type": "Type d'événement",
+    "Event Type": "Type d’événement",
     "Download this file": "Télécharger ce fichier",
     "Pin Message": "Épingler le message",
     "Failed to change settings": "Échec de la mise à jour des paramètres",
     "View Community": "Voir la communauté",
     "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Depuis votre navigateur actuel, le visuel et le ressenti de l'application pourraient être complètement erronés, et certaines fonctionnalités pourraient ne pas être supportées. Vous pouvez continuer malgré tout, mais vous n'aurez aucune aide si vous rencontrez des problèmes !",
     "Event sent!": "Événement envoyé !",
-    "Event Content": "Contenu de l'événement",
-    "Thank you!": "Merci !",
+    "Event Content": "Contenu de l’événement",
+    "Thank you!": "Merci !",
     "When I'm invited to a room": "Quand je suis invité dans un salon",
-    "Checking for an update...": "Recherche de mise à jour...",
+    "Checking for an update...": "Recherche de mise à jour…",
     "Logs sent": "Journaux envoyés",
-    "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Les journaux de débogage contiennent des données d'usage de l'application qui incluent votre nom d'utilisateur, les identifiants ou alias des salons ou groupes auxquels vous avez rendu visite ainsi que les noms des autres utilisateurs. Ils ne contiennent aucun message.",
-    "Failed to send logs: ": "Échec lors de l'envoi des journaux : ",
-    "Preparing to send logs": "Préparation d'envoi des journaux",
+    "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Les journaux de débogage contiennent des données d'usage de l’application qui incluent votre nom d’utilisateur, les identifiants ou alias des salons ou groupes auxquels vous avez rendu visite ainsi que les noms des autres utilisateurs. Ils ne contiennent aucun message.",
+    "Failed to send logs: ": "Échec lors de l’envoi des journaux : ",
+    "Preparing to send logs": "Préparation de l’envoi des journaux",
     "Missing roomId.": "Identifiant de salon manquant.",
     "Popout widget": "Détacher le widget",
     "Every page you use in the app": "Toutes les pages que vous utilisez dans l’application",
@@ -792,46 +792,46 @@
     "Clear Storage and Sign Out": "Effacer le stockage et se déconnecter",
     "Refresh": "Rafraîchir",
     "We encountered an error trying to restore your previous session.": "Une erreur est survenue lors de la restauration de la dernière session.",
-    "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Effacer le stockage de votre navigateur peut résoudre le problème, mais cela vous déconnectera et tous les historiques de conversation encryptés seront illisibles.",
-    "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Impossible de charger l'événement auquel il a été répondu, soit il n'existe pas, soit vous n'avez pas l'autorisation de le voir.",
+    "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Effacer le stockage de votre navigateur peut résoudre le problème. Ceci vous déconnectera et tous les historiques de conversations chiffrées seront illisibles.",
+    "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Impossible de charger l’événement auquel il a été répondu, soit il n’existe pas, soit vous n'avez pas l’autorisation de le voir.",
     "Collapse Reply Thread": "Masquer le fil de réponse",
-    "Enable widget screenshots on supported widgets": "Activer les captures d'écran des widgets pris en charge",
-    "Send analytics data": "Envoyer les données analytiques",
+    "Enable widget screenshots on supported widgets": "Activer les captures d’écran pour les widgets pris en charge",
+    "Send analytics data": "Envoyer les données de télémétrie",
     "Muted Users": "Utilisateurs ignorés",
     "Terms and Conditions": "Conditions générales",
-    "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Pour continuer à utiliser le serveur d'accueil %(homeserverDomain)s, vous devez lire et accepter nos conditions générales.",
+    "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Pour continuer à utiliser le serveur d’accueil %(homeserverDomain)s, vous devez lire et accepter nos conditions générales.",
     "Review terms and conditions": "Voir les conditions générales",
     "To continue, please enter your password:": "Pour continuer, veuillez renseigner votre mot de passe :",
-    "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Votre compte sera inutilisable de façon permanente. Vous ne pourrez plus vous reconnecter et personne ne pourra se réenregistrer avec le même identifiant d'utilisateur. Votre compte quittera tous les salons auxquels il participe et tous ses détails seront supprimés du serveur d'identité. <b>Cette action est irréversible.</b>",
+    "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Votre compte sera inutilisable de façon permanente. Vous ne pourrez plus vous reconnecter et personne ne pourra se réenregistrer avec le même identifiant d'utilisateur. Votre compte quittera tous les salons auxquels il participe et tous ses détails seront supprimés du serveur d’identité. <b>Cette action est irréversible.</b>",
     "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "La désactivation du compte <b>ne nous fait pas oublier les messages que vous avez envoyés par défaut.</b> Si vous souhaitez que nous les oubliions, cochez la case ci-dessous.",
     "e.g. %(exampleValue)s": "par ex. %(exampleValue)s",
     "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "La visibilité des messages dans Matrix est la même que celle des e-mails. Quand nous oublions vos messages, cela signifie que les messages que vous avez envoyés ne seront partagés avec aucun nouvel utilisateur ou avec les utilisateurs non enregistrés, mais les utilisateurs enregistrés qui ont déjà eu accès à ces messages en conserveront leur propre copie.",
-    "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Veuillez oublier tous les messages que j'ai envoyé quand mon compte sera désactivé (<b>Avertissement :</b> les futurs utilisateurs verront des conversations incomplètes)",
-    "Can't leave Server Notices room": "Impossible de quitter le salon des Annonces du serveur",
-    "This room is used for important messages from the Homeserver, so you cannot leave it.": "Ce salon est utilisé pour les messages importants du serveur d'accueil, donc vous ne pouvez pas en partir.",
+    "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Veuillez oublier tous les messages que j’ai envoyé quand mon compte sera désactivé (<b>Avertissement :</b> les futurs utilisateurs verront des conversations incomplètes)",
+    "Can't leave Server Notices room": "Impossible de quitter le salon des annonces au serveur",
+    "This room is used for important messages from the Homeserver, so you cannot leave it.": "Ce salon est utilisé pour les messages importants du serveur d’accueil, vous ne pouvez donc pas en partir.",
     "No Audio Outputs detected": "Aucune sortie audio détectée",
     "Audio Output": "Sortie audio",
-    "Share Link to User": "Partager le lien vers l'utilisateur",
+    "Share Link to User": "Partager le lien vers l’utilisateur",
     "Share room": "Partager le salon",
     "Share Room": "Partager le salon",
     "Link to most recent message": "Lien vers le message le plus récent",
-    "Share User": "Partager l'utilisateur",
+    "Share User": "Partager l’utilisateur",
     "Share Community": "Partager la communauté",
     "Share Room Message": "Partager le message du salon",
     "Link to selected message": "Lien vers le message sélectionné",
     "COPY": "COPIER",
     "Share Message": "Partager le message",
-    "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Dans les salons chiffrés, comme celui-ci, l'aperçu des liens est désactivé par défaut pour s'assurer que le serveur d'accueil (où sont générés les aperçus) ne puisse pas collecter d'informations sur les liens qui apparaissent dans ce salon.",
-    "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Quand quelqu'un met un lien dans son message, un aperçu du lien peut être affiché afin de fournir plus d'informations sur ce lien comme le titre, la description et une image du site.",
+    "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "Dans les salons chiffrés, comme celui-ci, l’aperçu des liens est désactivé par défaut pour s’assurer que le serveur d’accueil (où sont générés les aperçus) ne puisse pas collecter d’informations sur les liens qui apparaissent dans ce salon.",
+    "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Quand quelqu’un met un lien dans son message, un aperçu du lien peut être affiché afin de fournir plus d’informations sur ce lien comme le titre, la description et une image du site.",
     "The email field must not be blank.": "Le champ de l'adresse e-mail ne doit pas être vide.",
     "The phone number field must not be blank.": "Le champ du numéro de téléphone ne doit pas être vide.",
     "The password field must not be blank.": "Le champ du mot de passe ne doit pas être vide.",
     "Call in Progress": "Appel en cours",
     "A call is already in progress!": "Un appel est déjà en cours !",
-    "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "Vous ne pouvez voir aucun message tant que vous ne lisez et n'acceptez pas nos <consentLink>conditions générales</consentLink>.",
+    "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "Vous ne pouvez voir aucun message tant que vous ne lisez et n’acceptez pas nos <consentLink>conditions générales</consentLink>.",
     "Demote yourself?": "Vous rétrograder ?",
     "Demote": "Rétrograder",
-    "This event could not be displayed": "Cet événement n'a pas pu être affiché",
+    "This event could not be displayed": "Cet événement n’a pas pu être affiché",
     "Permission Required": "Autorisation requise",
     "You do not have permission to start a conference call in this room": "Vous n’avez pas l’autorisation de lancer un appel en téléconférence dans ce salon",
     "A call is currently being placed!": "Un appel est en cours !",
@@ -839,51 +839,51 @@
     "An error ocurred whilst trying to remove the widget from the room": "Une erreur est survenue lors de la suppression du widget du salon",
     "System Alerts": "Alertes système",
     "Only room administrators will see this warning": "Seuls les administrateurs du salon verront cet avertissement",
-    "Please <a>contact your service administrator</a> to continue using the service.": "Veuillez <a>contacter l'administrateur de votre service</a> pour continuer à l'utiliser.",
-    "This homeserver has hit its Monthly Active User limit.": "Ce serveur d'accueil a atteint sa limite mensuelle d'utilisateurs actifs.",
-    "This homeserver has exceeded one of its resource limits.": "Ce serveur d'accueil a dépassé une de ses limites de ressources.",
+    "Please <a>contact your service administrator</a> to continue using the service.": "Veuillez <a>contacter l’administrateur de votre service</a> pour continuer à l’utiliser.",
+    "This homeserver has hit its Monthly Active User limit.": "Ce serveur d’accueil a atteint sa limite mensuelle d'utilisateurs actifs.",
+    "This homeserver has exceeded one of its resource limits.": "Ce serveur d’accueil a dépassé une de ses limites de ressources.",
     "Upgrade Room Version": "Mettre à niveau la version du salon",
     "Create a new room with the same name, description and avatar": "Créer un salon avec le même nom, la même description et le même avatar",
-    "Update any local room aliases to point to the new room": "Mettre à jour tous les alias du salon locaux pour qu'ils dirigent vers le nouveau salon",
-    "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Empêcher les utilisateurs de discuter dans l'ancienne version du salon et envoyer un message conseillant aux nouveaux utilisateurs d'aller dans le nouveau salon",
-    "Put a link back to the old room at the start of the new room so people can see old messages": "Fournir un lien vers l'ancien salon au début du nouveau salon pour que l'on puisse voir les vieux messages",
-    "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Votre message n'a pas été envoyé car le serveur d'accueil a atteint sa limite mensuelle d'utilisateurs. Veuillez <a>contacter l'administrateur de votre service</a> pour continuer à l'utiliser.",
-    "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Votre message n'a pas été envoyé car ce serveur d'accueil a dépassé une de ses limites de ressources. Veuillez <a>contacter l'administrateur de votre service</a> pour continuer à l'utiliser.",
-    "Please <a>contact your service administrator</a> to continue using this service.": "Veuillez <a>contacter l'administrateur de votre service</a> pour continuer à l'utiliser.",
-    "Sorry, your homeserver is too old to participate in this room.": "Désolé, votre serveur d'accueil est trop vieux pour participer à ce salon.",
-    "Please contact your homeserver administrator.": "Veuillez contacter l'administrateur de votre serveur d'accueil.",
+    "Update any local room aliases to point to the new room": "Mettre à jour tous les alias du salon locaux pour qu’ils dirigent vers le nouveau salon",
+    "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Empêcher les utilisateurs de discuter dans l’ancienne version du salon et envoyer un message conseillant aux nouveaux utilisateurs d’aller dans le nouveau salon",
+    "Put a link back to the old room at the start of the new room so people can see old messages": "Fournir un lien vers l’ancien salon au début du nouveau salon pour qu’il soit possible de consulter les anciens messages",
+    "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Votre message n’a pas été envoyé car le serveur d’accueil a atteint sa limite mensuelle d’utilisateurs. Veuillez <a>contacter l’administrateur de votre service</a> pour continuer à l’utiliser.",
+    "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Votre message n’a pas été envoyé car ce serveur d’accueil a dépassé une de ses limites de ressources. Veuillez <a>contacter l’administrateur de votre service</a> pour continuer à l’utiliser.",
+    "Please <a>contact your service administrator</a> to continue using this service.": "Veuillez <a>contacter l’administrateur de votre service</a> pour continuer à l’utiliser.",
+    "Sorry, your homeserver is too old to participate in this room.": "Désolé, votre serveur d’accueil est trop vieux pour participer à ce salon.",
+    "Please contact your homeserver administrator.": "Veuillez contacter l’administrateur de votre serveur d’accueil.",
     "Legal": "Légal",
-    "This room has been replaced and is no longer active.": "Ce salon a été remplacé et n'est plus actif.",
+    "This room has been replaced and is no longer active.": "Ce salon a été remplacé et n’est plus actif.",
     "The conversation continues here.": "La discussion continue ici.",
-    "This room is a continuation of another conversation.": "Ce salon est la suite d'une autre discussion.",
-    "Click here to see older messages.": "Cliquer ici pour voir les vieux messages.",
+    "This room is a continuation of another conversation.": "Ce salon est la suite d’une autre discussion.",
+    "Click here to see older messages.": "Cliquer ici pour voir les anciens messages.",
     "Failed to upgrade room": "Échec de la mise à niveau du salon",
-    "The room upgrade could not be completed": "La mise à niveau du salon n'a pas pu être effectuée",
+    "The room upgrade could not be completed": "La mise à niveau du salon n’a pas pu être effectuée",
     "Upgrade this room to version %(version)s": "Mettre à niveau ce salon vers la version %(version)s",
     "Forces the current outbound group session in an encrypted room to be discarded": "Force la session de groupe sortante actuelle dans un salon chiffré à être rejetée",
-    "Unable to connect to Homeserver. Retrying...": "Impossible de se connecter au serveur d'accueil. Reconnexion...",
+    "Unable to connect to Homeserver. Retrying...": "Impossible de se connecter au serveur d’accueil. Reconnexion…",
     "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s a défini l’adresse principale pour ce salon comme %(address)s.",
     "%(senderName)s removed the main address for this room.": "%(senderName)s a supprimé l’adresse principale de ce salon.",
-    "%(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 utilise maintenant 3 à 5 fois moins de mémoire, en ne chargeant les informations des autres utilisateurs que quand elles sont nécessaires. Veuillez patienter pendant que l'on se resynchronise avec le serveur !",
+    "%(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 utilise maintenant 3 à 5 fois moins de mémoire, en ne chargeant les informations des autres utilisateurs que quand elles sont nécessaires. Veuillez patienter pendant que l’on se resynchronise avec le serveur !",
     "Updating %(brand)s": "Mise à jour de %(brand)s",
     "Before submitting logs, you must <a>create a GitHub issue</a> to describe your problem.": "Avant de soumettre vos journaux, vous devez <a>créer une « issue » sur GitHub</a> pour décrire votre problème.",
-    "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.": "Vous avez utilisé auparavant %(brand)s sur %(host)s avec le chargement différé activé. Dans cette version le chargement différé est désactivé. Comme le cache local n'est pas compatible entre ces deux réglages, %(brand)s doit resynchroniser votre compte.",
-    "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Si l'autre version de %(brand)s est encore ouverte dans un autre onglet, merci de le fermer car l'utilisation de %(brand)s sur le même hôte avec le chargement différé activé et désactivé à la fois causera des problèmes.",
+    "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.": "Vous avez utilisé auparavant %(brand)s sur %(host)s avec le chargement différé activé. Dans cette version le chargement différé est désactivé. Comme le cache local n’est pas compatible entre ces deux réglages, %(brand)s doit resynchroniser votre compte.",
+    "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Si l’autre version de %(brand)s est encore ouverte dans un autre onglet, merci de le fermer car l’utilisation de %(brand)s sur le même hôte avec le chargement différé activé et désactivé à la fois causera des problèmes.",
     "Incompatible local cache": "Cache local incompatible",
     "Clear cache and resync": "Vider le cache et resynchroniser",
-    "Please review and accept the policies of this homeserver:": "Veuillez lire et accepter les politiques de ce serveur d'accueil :",
+    "Please review and accept the policies of this homeserver:": "Veuillez lire et accepter les politiques de ce serveur d’accueil :",
     "Add some now": "En ajouter maintenant",
-    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Vous êtes administrateur de cette communauté. Vous ne pourrez pas revenir sans une invitation d'un autre administrateur.",
+    "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "Vous êtes administrateur de cette communauté. Vous ne pourrez pas revenir sans une invitation d’un autre administrateur.",
     "Open Devtools": "Ouvrir les outils développeur",
     "Show developer tools": "Afficher les outils de développeur",
-    "Please review and accept all of the homeserver's policies": "Veuillez lire et accepter toutes les politiques du serveur d'accueil",
-    "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": "Pour éviter de perdre l'historique de vos discussions, vous devez exporter vos clés avant de vous déconnecter. Vous devez revenir à une version plus récente de %(brand)s pour pouvoir le faire",
+    "Please review and accept all of the homeserver's policies": "Veuillez lire et accepter toutes les politiques du serveur d’accueil",
+    "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": "Pour éviter de perdre l’historique de vos discussions, vous devez exporter vos clés avant de vous déconnecter. Vous devez revenir à une version plus récente de %(brand)s pour pouvoir le faire",
     "Incompatible Database": "Base de données incompatible",
     "Continue With Encryption Disabled": "Continuer avec le chiffrement désactivé",
-    "Sign in with single sign-on": "Se connecter avec l'authentification unique",
+    "Sign in with single sign-on": "Se connecter avec l’authentification unique",
     "Unable to load! Check your network connectivity and try again.": "Chargement impossible ! Vérifiez votre connexion au réseau et réessayez.",
     "Delete Backup": "Supprimer la sauvegarde",
-    "Unable to load key backup status": "Impossible de charger l'état de sauvegarde des clés",
+    "Unable to load key backup status": "Impossible de charger l’état de sauvegarde des clés",
     "Backup version: ": "Version de la sauvegarde : ",
     "Algorithm: ": "Algorithme : ",
     "Next": "Suivant",
@@ -897,28 +897,28 @@
     "Set up Secure Message Recovery": "Configurer la récupération de messages sécurisée",
     "Unable to create key backup": "Impossible de créer la sauvegarde des clés",
     "Retry": "Réessayer",
-    "Unable to load backup status": "Impossible de charger l'état de la sauvegarde",
+    "Unable to load backup status": "Impossible de récupérer l’état de la sauvegarde",
     "Unable to restore backup": "Impossible de restaurer la sauvegarde",
-    "No backup found!": "Aucune sauvegarde n'a été trouvée !",
+    "No backup found!": "Aucune sauvegarde n’a été trouvée !",
     "Failed to decrypt %(failedCount)s sessions!": "Le déchiffrement de %(failedCount)s sessions a échoué !",
     "Access your secure message history and set up secure messaging by entering your recovery passphrase.": "Accédez à l'historique sécurisé de vos messages et configurez la messagerie sécurisée en renseignant votre phrase de récupération.",
     "If you've forgotten your recovery passphrase you can <button1>use your recovery key</button1> or <button2>set up new recovery options</button2>": "Si vous avez oublié votre phrase de récupération vous pouvez <button1>utiliser votre clé de récupération</button1> ou <button2>configurer de nouvelles options de récupération</button2>",
     "This looks like a valid recovery key!": "Cela ressemble à une clé de récupération valide !",
     "Not a valid recovery key": "Ce n'est pas une clé de récupération valide",
     "Access your secure message history and set up secure messaging by entering your recovery key.": "Accédez à l'historique sécurisé de vos messages et configurez la messagerie sécurisée en renseignant votre clé de récupération.",
-    "Failed to perform homeserver discovery": "Échec lors de la découverte du serveur d'accueil",
-    "Invalid homeserver discovery response": "Réponse de découverte du serveur d'accueil non valide",
+    "Failed to perform homeserver discovery": "Échec lors de la découverte du serveur d’accueil",
+    "Invalid homeserver discovery response": "Réponse de découverte du serveur d’accueil non valide",
     "Use a few words, avoid common phrases": "Utilisez quelques mots, évitez les phrases courantes",
-    "No need for symbols, digits, or uppercase letters": "Il n'y a pas besoin de symboles, de chiffres ou de majuscules",
+    "No need for symbols, digits, or uppercase letters": "Il n'y a pas besoin de symbole, de chiffre ou de majuscule",
     "Avoid repeated words and characters": "Évitez de répéter des mots et des caractères",
     "Avoid sequences": "Évitez les séquences",
     "Avoid recent years": "Évitez les années récentes",
     "Avoid years that are associated with you": "Évitez les années qui ont un rapport avec vous",
     "Avoid dates and years that are associated with you": "Évitez les dates et les années qui ont un rapport avec vous",
-    "Capitalization doesn't help very much": "Les majuscules n'aident pas vraiment",
-    "All-uppercase is almost as easy to guess as all-lowercase": "Uniquement des majuscules, c'est presque aussi facile à deviner qu'uniquement des minuscules",
+    "Capitalization doesn't help very much": "Les majuscules n’aident pas vraiment",
+    "All-uppercase is almost as easy to guess as all-lowercase": "Uniquement des majuscules, c’est presque aussi facile à deviner qu’uniquement des minuscules",
     "Reversed words aren't much harder to guess": "Les mots inversés ne sont pas beaucoup plus difficiles à deviner",
-    "Predictable substitutions like '@' instead of 'a' don't help very much": "Les substitutions prévisibles comme « @ » à la place de « a » n'aident pas vraiment",
+    "Predictable substitutions like '@' instead of 'a' don't help very much": "Les substitutions prévisibles comme « @ » à la place de « a » ne sont pas très utiles",
     "Add another word or two. Uncommon words are better.": "Ajoutez un ou deux mots. Les mots rares sont à privilégier.",
     "Repeats like \"aaa\" are easy to guess": "Les répétitions comme « aaa » sont faciles à deviner",
     "Repeats like \"abcabcabc\" are only slightly harder to guess than \"abc\"": "Les répétitions comme « abcabcabc » ne sont pas beaucoup plus difficiles à deviner que « abc »",
@@ -927,7 +927,7 @@
     "Dates are often easy to guess": "Les dates sont généralement faciles à deviner",
     "This is a top-10 common password": "Cela fait partie des 10 mots de passe les plus répandus",
     "This is a top-100 common password": "Cela fait partie des 100 mots de passe les plus répandus",
-    "This is a very common password": "C'est un mot de passe très répandu",
+    "This is a very common password": "C’est un mot de passe très répandu",
     "This is similar to a commonly used password": "Cela ressemble à un mot de passe répandu",
     "A word by itself is easy to guess": "Un mot seul est facile à deviner",
     "Names and surnames by themselves are easy to guess": "Les noms et prénoms seuls sont faciles à deviner",
@@ -936,8 +936,8 @@
     "Failed to load group members": "Échec du chargement des membres du groupe",
     "Failed to invite users to the room:": "Échec de l’invitation d'utilisateurs dans le salon :",
     "There was an error joining the room": "Une erreur est survenue en rejoignant le salon",
-    "You do not have permission to invite people to this room.": "Vous n'avez pas la permission d'envoyer des invitations dans ce salon.",
-    "User %(user_id)s does not exist": "L'utilisateur %(user_id)s n'existe pas",
+    "You do not have permission to invite people to this room.": "Vous n’avez pas la permission d’envoyer des invitations dans ce salon.",
+    "User %(user_id)s does not exist": "L’utilisateur %(user_id)s n’existe pas",
     "Unknown server error": "Erreur de serveur inconnue",
     "Show a reminder to enable Secure Message Recovery in encrypted rooms": "Afficher un rappel pour activer la récupération de messages sécurisée dans les salons chiffrés",
     "Don't ask again": "Ne plus me demander",
@@ -952,23 +952,23 @@
     "Invalid identity server discovery response": "Réponse non valide lors de la découverte du serveur d'identité",
     "General failure": "Erreur générale",
     "New Recovery Method": "Nouvelle méthode de récupération",
-    "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si vous n'avez pas activé de nouvelle méthode de récupération, un attaquant essaye peut-être d'accéder à votre compte. Changez immédiatement le mot de passe de votre compte et configurez une nouvelle méthode de récupération dans les paramètres.",
+    "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si vous n’avez pas activé de nouvelle méthode de récupération, un attaquant essaye peut-être d’accéder à votre compte. Changez immédiatement le mot de passe de votre compte et configurez une nouvelle méthode de récupération dans les paramètres.",
     "Set up Secure Messages": "Configurer les messages sécurisés",
     "Go to Settings": "Aller aux paramètres",
     "Straight rows of keys are easy to guess": "Les suites de touches sont faciles à deviner",
     "Short keyboard patterns are easy to guess": "Les répétitions de motif court sur un clavier sont faciles à deviner",
-    "Custom user status messages": "Messages de statut de l'utilisateur personnalisés",
-    "Unable to load commit detail: %(msg)s": "Impossible de charger les détails de l'envoi : %(msg)s",
+    "Custom user status messages": "Messages de statut de l’utilisateur personnalisés",
+    "Unable to load commit detail: %(msg)s": "Impossible de charger les détails de l’envoi : %(msg)s",
     "Set a new status...": "Configurer un nouveau statut…",
     "Clear status": "Effacer le statut",
     "Unrecognised address": "Adresse non reconnue",
-    "User %(user_id)s may or may not exist": "L'utilisateur %(user_id)s pourrait exister",
+    "User %(user_id)s may or may not exist": "L’utilisateur %(user_id)s pourrait exister",
     "The following users may not exist": "Les utilisateurs suivants pourraient ne pas exister",
-    "Prompt before sending invites to potentially invalid matrix IDs": "Demander avant d'envoyer des invitations à des identifiants matrix potentiellement non valides",
+    "Prompt before sending invites to potentially invalid matrix IDs": "Demander avant d’envoyer des invitations à des identifiants matrix potentiellement non valides",
     "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Impossible de trouver les profils pour les identifiants Matrix listés ci-dessous. Voulez-vous quand même les inviter ?",
     "Invite anyway and never warn me again": "Inviter quand même et ne plus me prévenir",
     "Invite anyway": "Inviter quand même",
-    "Whether or not you're logged in (we don't record your username)": "Si vous êtes connecté ou pas (votre nom d'utilisateur n’est pas enregistré)",
+    "Whether or not you're logged in (we don't record your username)": "Si vous êtes connecté ou pas (votre nom d’utilisateur n’est pas enregistré)",
     "Upgrades a room to a new version": "Met à niveau un salon vers une nouvelle version",
     "Sets the room name": "Définit le nom du salon",
     "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s a mis à niveau ce salon.",
@@ -979,15 +979,15 @@
     "Enable Emoji suggestions while typing": "Activer la suggestion d’émojis lors de la saisie",
     "Render simple counters in room header": "Afficher des compteurs simples dans l’en-tête des salons",
     "Show a placeholder for removed messages": "Afficher les messages supprimés",
-    "Show join/leave messages (invites/kicks/bans unaffected)": "Afficher les messages d'arrivée et de départ (les invitations/expulsions/bannissements ne sont pas concernés)",
-    "Show avatar changes": "Afficher les changements d'avatar",
-    "Show display name changes": "Afficher les changements de nom affiché",
-    "Show avatars in user and room mentions": "Afficher les avatars dans les mentions d'utilisateur et de salon",
+    "Show join/leave messages (invites/kicks/bans unaffected)": "Afficher les messages d’arrivée et de départ (les invitations/expulsions/bannissements ne sont pas concernés)",
+    "Show avatar changes": "Afficher les changements d’avatar",
+    "Show display name changes": "Afficher les changements de nom d’affichage",
+    "Show avatars in user and room mentions": "Afficher les avatars dans les mentions d’utilisateur et de salon",
     "Enable big emoji in chat": "Activer les gros émojis dans les discussions",
     "Send typing notifications": "Envoyer des notifications de saisie",
     "Enable Community Filter Panel": "Activer le panneau de filtrage de communauté",
-    "Messages containing my username": "Messages contenant mon nom d'utilisateur",
-    "The other party cancelled the verification.": "L'autre personne a annulé la vérification.",
+    "Messages containing my username": "Messages contenant mon nom d’utilisateur",
+    "The other party cancelled the verification.": "L’autre personne a annulé la vérification.",
     "Verified!": "Vérifié !",
     "You've successfully verified this user.": "Vous avez vérifié cet utilisateur avec succès.",
     "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Les messages sécurisés avec cet utilisateur sont chiffrés de bout en bout et ne peuvent être lus par d’autres personnes.",
@@ -995,16 +995,16 @@
     "Verify this user by confirming the following number appears on their screen.": "Vérifier cet utilisateur en confirmant que le nombre suivant apparaît sur leur écran.",
     "Yes": "Oui",
     "No": "Non",
-    "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Nous vous avons envoyé un e-mail pour vérifier votre adresse. Veuillez suivre les instructions qu'il contient puis cliquer sur le bouton ci-dessous.",
+    "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Nous vous avons envoyé un e-mail pour vérifier votre adresse. Veuillez suivre les instructions qu’il contient puis cliquer sur le bouton ci-dessous.",
     "Email Address": "Adresse e-mail",
-    "Backing up %(sessionsRemaining)s keys...": "Sauvegarde de %(sessionsRemaining)s clés...",
+    "Backing up %(sessionsRemaining)s keys...": "Sauvegarde de %(sessionsRemaining)s clés…",
     "All keys backed up": "Toutes les clés ont été sauvegardées",
     "Add an email address to configure email notifications": "Ajouter une adresse e-mail pour configurer les notifications par e-mail",
     "Unable to verify phone number.": "Impossible de vérifier le numéro de téléphone.",
     "Verification code": "Code de vérification",
     "Phone Number": "Numéro de téléphone",
     "Profile picture": "Image de profil",
-    "Display Name": "Nom affiché",
+    "Display Name": "Nom d’affichage",
     "Room information": "Information du salon",
     "Internal room ID:": "Identifiant interne du salon :",
     "Room version": "Version du salon",
@@ -1018,37 +1018,37 @@
     "Language and region": "Langue et région",
     "Theme": "Thème",
     "Account management": "Gestion du compte",
-    "Deactivating your account is a permanent action - be careful!": "La désactivation du compte est une action permanente. Soyez prudent !",
-    "For help with using %(brand)s, click <a>here</a>.": "Pour obtenir de l'aide sur l'utilisation de %(brand)s, cliquez <a>ici</a>.",
-    "For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "Pour obtenir de l'aide sur l'utilisation de %(brand)s, cliquez <a>ici</a> ou commencez une discussion avec notre bot en utilisant le bouton ci-dessous.",
-    "Help & About": "Aide & À propos",
-    "Bug reporting": "Signalement d'anomalies",
+    "Deactivating your account is a permanent action - be careful!": "La désactivation du compte est une action définitive. Soyez prudents !",
+    "For help with using %(brand)s, click <a>here</a>.": "Pour obtenir de l’aide sur l’utilisation de %(brand)s, cliquez <a>ici</a>.",
+    "For help with using %(brand)s, click <a>here</a> or start a chat with our bot using the button below.": "Pour obtenir de l’aide sur l’utilisation de %(brand)s, cliquez <a>ici</a> ou commencez une discussion avec notre bot en utilisant le bouton ci-dessous.",
+    "Help & About": "Aide et À propos",
+    "Bug reporting": "Signalement d’anomalies",
     "FAQ": "FAQ",
     "Versions": "Versions",
     "Preferences": "Préférences",
     "Composer": "Compositeur",
     "Room list": "Liste de salons",
-    "Timeline": "Historique",
-    "Autocomplete delay (ms)": "Délai pour l'autocomplétion (ms)",
+    "Timeline": "Fil de discussion",
+    "Autocomplete delay (ms)": "Retard pour l’autocomplétion (ms)",
     "Chat with %(brand)s Bot": "Discuter avec le bot %(brand)s",
-    "Roles & Permissions": "Rôles & Permissions",
-    "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Les modifications concernant l'accès à l'historique ne s'appliqueront qu'aux futurs messages de ce salon. La visibilité de l'historique existant ne sera pas modifiée.",
-    "Security & Privacy": "Sécurité & Vie privée",
+    "Roles & Permissions": "Rôles et permissions",
+    "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Les modifications concernant l'accès à l’historique ne s'appliqueront qu’aux futurs messages de ce salon. La visibilité de l’historique existant ne sera pas modifiée.",
+    "Security & Privacy": "Sécurité et vie privée",
     "Encryption": "Chiffrement",
-    "Once enabled, encryption cannot be disabled.": "Le chiffrement ne peut pas être désactivé une fois qu'il a été activé.",
+    "Once enabled, encryption cannot be disabled.": "Le chiffrement ne peut pas être désactivé une fois qu’il a été activé.",
     "Encrypted": "Chiffré",
     "Ignored users": "Utilisateurs ignorés",
     "Bulk options": "Options de groupe",
     "Key backup": "Sauvegarde de clés",
     "Missing media permissions, click the button below to request.": "Permissions multimédia manquantes, cliquez sur le bouton ci-dessous pour la demander.",
     "Request media permissions": "Demander les permissions multimédia",
-    "Voice & Video": "Voix & Vidéo",
-    "Main address": "Adresse princpale",
+    "Voice & Video": "Audio et vidéo",
+    "Main address": "Adresse principale",
     "Room avatar": "Avatar du salon",
     "Room Name": "Nom du salon",
     "Room Topic": "Sujet du salon",
     "Join": "Rejoindre",
-    "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Vérifier cet utilisateur pour le marquer comme fiable. Faire confiance aux utilisateurs vous permet d’être serein quand vous utilisez des messages chiffrés de bout en bout.",
+    "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Vérifier cet utilisateur pour le marquer comme fiable. Faire confiance aux utilisateurs vous permet d’être tranquille lorsque vous utilisez des messages chiffrés de bout en bout.",
     "Waiting for partner to confirm...": "Nous attendons que le partenaire confirme…",
     "Incoming Verification Request": "Demande de vérification entrante",
     "Go back": "Revenir en arrière",
@@ -1060,7 +1060,7 @@
     "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of <a>modular.im</a>.": "Saisissez l'emplacement de votre serveur d'accueil Modular. Il peut utiliser votre nom de domaine personnel ou être un sous-domaine de <a>modular.im</a>.",
     "Server Name": "Nom du serveur",
     "The username field must not be blank.": "Le champ du nom d'utilisateur ne doit pas être vide.",
-    "Username": "Nom d'utilisateur",
+    "Username": "Nom d’utilisateur",
     "Not sure of your password? <a>Set a new one</a>": "Vous n'êtes pas sûr(e) de votre mot de passe ? <a>Changez-le</a>",
     "Create your account": "Créer votre compte",
     "Email (optional)": "E-mail (facultatif)",
@@ -1070,20 +1070,20 @@
     "Homeserver URL": "URL du serveur d'accueil",
     "Identity Server URL": "URL du serveur d'identité",
     "Free": "Gratuit",
-    "Join millions for free on the largest public server": "Rejoignez des millions d'utilisateurs gratuitement sur le plus grand serveur public",
+    "Join millions for free on the largest public server": "Rejoignez des millions d’utilisateurs gratuitement sur le plus grand serveur public",
     "Premium": "Premium",
     "Premium hosting for organisations <a>Learn more</a>": "Hébergement premium pour les organisations <a>En savoir plus</a>",
     "Other": "Autre",
     "Find other public servers or use a custom server": "Trouvez d'autres serveurs publics ou utilisez un serveur personnalisé",
     "Guest": "Visiteur",
-    "Sign in instead": "Se connecter",
+    "Sign in instead": "Se connecter plutôt",
     "Set a new password": "Définir un nouveau mot de passe",
     "Create account": "Créer un compte",
     "Keep going...": "Continuer…",
     "Starting backup...": "Début de la sauvegarde…",
     "A new recovery passphrase and key for Secure Messages have been detected.": "Un nouveau mot de passe et une nouvelle clé de récupération pour les messages sécurisés ont été détectés.",
     "Recovery Method Removed": "Méthode de récupération supprimée",
-    "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si vous n'avez pas supprimé la méthode de récupération, un attaquant peut être en train d'essayer d'accéder à votre compte. Modifiez le mot de passe de votre compte et configurez une nouvelle méthode de récupération dans les réglages.",
+    "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Si vous n’avez pas supprimé la méthode de récupération, un attaquant peut être en train d’essayer d’accéder à votre compte. Modifiez le mot de passe de votre compte et configurez une nouvelle méthode de récupération dans les réglages.",
     "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Le fichier « %(fileName)s » dépasse la taille limite autorisée par ce serveur pour les envois",
     "Gets or sets the room topic": "Récupère ou définit le sujet du salon",
     "This room has no topic.": "Ce salon n'a pas de sujet.",
@@ -1158,16 +1158,16 @@
     "Headphones": "Écouteurs",
     "Folder": "Dossier",
     "Pin": "Épingle",
-    "This homeserver would like to make sure you are not a robot.": "Ce serveur d'accueil veut s'assurer que vous n'êtes pas un robot.",
+    "This homeserver would like to make sure you are not a robot.": "Ce serveur d’accueil veut s’assurer que vous n’êtes pas un robot.",
     "Change": "Changer",
     "Couldn't load page": "Impossible de charger la page",
-    "This homeserver does not support communities": "Ce serveur d'accueil ne prend pas en charge les communautés",
+    "This homeserver does not support communities": "Ce serveur d’accueil ne prend pas en charge les communautés",
     "A verification email will be sent to your inbox to confirm setting your new password.": "Un e-mail de vérification sera envoyé à votre adresse pour confirmer la modification de votre mot de passe.",
     "Your password has been reset.": "Votre mot de passe a été réinitialisé.",
-    "This homeserver does not support login using email address.": "Ce serveur d'accueil ne prend pas en charge la connexion avec une adresse e-mail.",
-    "Registration has been disabled on this homeserver.": "L'inscription a été désactivée sur ce serveur d'accueil.",
-    "Unable to query for supported registration methods.": "Impossible de demander les méthodes d'inscription prises en charge.",
-    "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "En êtes-vous sûr(e) ? Vous perdrez vos messages chiffrés si vos clés ne sont pas sauvegardées correctement.",
+    "This homeserver does not support login using email address.": "Ce serveur d’accueil ne prend pas en charge la connexion avec une adresse e-mail.",
+    "Registration has been disabled on this homeserver.": "L’inscription a été désactivée sur ce serveur d’accueil.",
+    "Unable to query for supported registration methods.": "Impossible de demander les méthodes d’inscription prises en charge.",
+    "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "En êtes-vous sûr ? Vous perdrez vos messages chiffrés si vos clés ne sont pas sauvegardées correctement.",
     "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Les messages chiffrés sont sécurisés avec un chiffrement de bout en bout. Seuls vous et le(s) destinataire(s) ont les clés pour lire ces messages.",
     "Restore from Backup": "Restaurer depuis la sauvegarde",
     "Back up your keys before signing out to avoid losing them.": "Sauvegardez vos clés avant de vous déconnecter pour éviter de les perdre.",
@@ -1189,7 +1189,7 @@
     "Allow Peer-to-Peer for 1:1 calls": "Autoriser les connexions pair-à-pair pour les appels individuels",
     "Credits": "Crédits",
     "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Si vous avez rencontré des problèmes ou si vous souhaitez partager votre avis, dites-le nous sur GitHub.",
-    "Changes your display nickname in the current room only": "Change votre nom affiché seulement dans le salon actuel",
+    "Changes your display nickname in the current room only": "Change votre nom d’affichage seulement dans le salon actuel",
     "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s a activé le badge pour %(groups)s dans ce salon.",
     "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s a désactivé le badge pour %(groups)s dans ce salon.",
     "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s a activé le badge pour %(newGroups)s et désactivé le badge pour %(oldGroups)s dans ce salon.",
@@ -1242,7 +1242,7 @@
     "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "Ce salon utilise la version <roomVersion />, que ce serveur d’accueil a marqué comme <i>instable</i>.",
     "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "La mise à niveau du salon désactivera cette instance du salon et créera un salon mis à niveau avec le même nom.",
     "Failed to revoke invite": "Échec de la révocation de l’invitation",
-    "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Impossible de révoquer l’invitation. Le serveur subi peut-être un problème temporaire ou vous n’avez pas la permission de révoquer l’invitation.",
+    "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Impossible de révoquer l’invitation. Le serveur subit peut-être un problème temporaire ou vous n’avez pas la permission de révoquer l’invitation.",
     "Revoke invite": "Révoquer l’invitation",
     "Invited by %(sender)s": "Invité par %(sender)s",
     "Maximize apps": "Maximiser les applications",
@@ -1295,11 +1295,11 @@
     "Join the conversation with an account": "Rejoindre la conversation avec un compte",
     "Sign Up": "S’inscrire",
     "Sign In": "Se connecter",
-    "You were kicked from %(roomName)s by %(memberName)s": "Vous avez été expulsé(e) de %(roomName)s par %(memberName)s",
+    "You were kicked from %(roomName)s by %(memberName)s": "Vous avez été expulsé de %(roomName)s par %(memberName)s",
     "Reason: %(reason)s": "Motif : %(reason)s",
     "Forget this room": "Oublier ce salon",
     "Re-join": "Revenir",
-    "You were banned from %(roomName)s by %(memberName)s": "Vous avez été banni(e) de %(roomName)s par %(memberName)s",
+    "You were banned from %(roomName)s by %(memberName)s": "Vous avez été banni de %(roomName)s par %(memberName)s",
     "Something went wrong with your invite to %(roomName)s": "Une erreur est survenue avec votre invitation à %(roomName)s",
     "You can only join it with a working invite.": "Vous ne pouvez le rejoindre qu’avec une invitation fonctionnelle.",
     "You can still join it because this is a public room.": "Vous pouvez quand même le rejoindre car c’est un salon public.",
@@ -1307,7 +1307,7 @@
     "Try to join anyway": "Essayer de le rejoindre quand même",
     "Do you want to chat with %(user)s?": "Voulez-vous discuter avec %(user)s ?",
     "Do you want to join %(roomName)s?": "Voulez-vous rejoindre %(roomName)s ?",
-    "<userName/> invited you": "<userName/> vous a invité(e)",
+    "<userName/> invited you": "<userName/> vous a invité",
     "You're previewing %(roomName)s. Want to join it?": "Ceci est un aperçu de %(roomName)s. Voulez-vous rejoindre le salon ?",
     "%(roomName)s can't be previewed. Do you want to join it?": "Vous ne pouvez pas avoir d’aperçu de %(roomName)s. Voulez-vous rejoindre le salon ?",
     "This room doesn't exist. Are you sure you're at the right place?": "Ce salon n’existe pas. Êtes-vous vraiment au bon endroit ?",
@@ -1315,7 +1315,7 @@
     "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.": "%(errcode)s a été retourné en essayant d’accéder au salon. Si vous pensez que vous ne devriez pas voir ce message, veuillez <issueLink>soumettre un rapport d’anomalie</issueLink>.",
     "This room has already been upgraded.": "Ce salon a déjà été mis à niveau.",
     "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>ont réagi avec %(shortName)s</reactedWith>",
-    "edited": "édité",
+    "edited": "modifié",
     "Rotate Left": "Tourner à gauche",
     "Rotate Right": "Tourner à droite",
     "View Servers in Room": "Voir les serveurs dans le salon",
@@ -1336,10 +1336,10 @@
     "Homeserver URL does not appear to be a valid Matrix homeserver": "L’URL du serveur d’accueil ne semble pas être un serveur d’accueil Matrix valide",
     "Invalid base_url for m.identity_server": "base_url pour m.identity_server non valide",
     "Identity server URL does not appear to be a valid identity server": "L’URL du serveur d’identité ne semble pas être un serveur d’identité valide",
-    "Show hidden events in timeline": "Afficher les évènements cachés dans l’historique",
+    "Show hidden events in timeline": "Afficher les évènements cachés dans le fil de discussion",
     "Your profile": "Votre profil",
     "Add room": "Ajouter un salon",
-    "Edit message": "Éditer le message",
+    "Edit message": "Modifier le message",
     "No homeserver URL provided": "Aucune URL de serveur d’accueil fournie",
     "Unexpected error resolving homeserver configuration": "Une erreur inattendue est survenue pendant la résolution de la configuration du serveur d’accueil",
     "Unable to validate homeserver/identity server": "Impossible de valider le serveur d’accueil/d’identité",
@@ -1369,8 +1369,8 @@
     "Upload all": "Tout envoyer",
     "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "Votre nouveau compte (%(newAccountId)s) est créé, mais vous êtes déjà connecté avec un autre compte (%(loggedInUserId)s).",
     "Continue with previous account": "Continuer avec le compte précédent",
-    "Edited at %(date)s. Click to view edits.": "Édité à %(date)s. Cliquer pour voir les éditions.",
-    "Message edits": "Éditions du message",
+    "Edited at %(date)s. Click to view edits.": "Modifié le %(date)s. Cliquer pour voir les modifications.",
+    "Message edits": "Modifications du message",
     "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "La mise à niveau de ce salon nécessite de fermer l’instance actuelle du salon et de créer un nouveau salon à la place. Pour fournir la meilleure expérience possible aux utilisateurs, nous allons :",
     "Loading room preview": "Chargement de l’aperçu du salon",
     "Show all": "Tout afficher",
@@ -1379,12 +1379,12 @@
     "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s n’ont fait aucun changement",
     "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s n’a fait aucun changement %(count)s fois",
     "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s n’a fait aucun changement",
-    "Resend edit": "Renvoyer l’édition",
+    "Resend edit": "Renvoyer la modification",
     "Resend %(unsentCount)s reaction(s)": "Renvoyer %(unsentCount)s réaction(s)",
     "Resend removal": "Renvoyer la suppression",
     "Your homeserver doesn't seem to support this feature.": "Il semble que votre serveur d’accueil ne prenne pas en charge cette fonctionnalité.",
     "Changes your avatar in all rooms": "Change votre avatar dans tous les salons",
-    "You're signed out": "Vous êtes déconnecté(e)",
+    "You're signed out": "Vous êtes déconnecté",
     "Clear all data": "Supprimer toutes les données",
     "Removing…": "Suppression…",
     "Failed to re-authenticate due to a homeserver problem": "Échec de la ré-authentification à cause d’un problème du serveur d’accueil",
@@ -1397,8 +1397,8 @@
     "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Dites-nous ce qui s’est mal passé ou, encore mieux, créez un rapport d’erreur sur GitHub qui décrit le problème.",
     "Identity Server": "Serveur d’identité",
     "Find others by phone or email": "Trouver d’autres personnes par téléphone ou e-mail",
-    "Be found by phone or email": "Être trouvé(e) par téléphone ou e-mail",
-    "Use bots, bridges, widgets and sticker packs": "Utiliser des robots, des passerelles, des widgets ou des packs de stickers",
+    "Be found by phone or email": "Être trouvé par téléphone ou e-mail",
+    "Use bots, bridges, widgets and sticker packs": "Utiliser des robots, des passerelles, des widgets ou des jeux d’autocollants",
     "Terms of Service": "Conditions de service",
     "Service": "Service",
     "Summary": "Résumé",
@@ -1418,14 +1418,14 @@
     "Unable to share phone number": "Impossible de partager le numéro de téléphone",
     "Please enter verification code sent via text.": "Veuillez saisir le code de vérification envoyé par SMS.",
     "Discovery options will appear once you have added a phone number above.": "Les options de découverte apparaîtront quand vous aurez ajouté un numéro de téléphone ci-dessus.",
-    "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Un message textuel a été envoyé à +%(msisdn)s. Saisissez le code de vérification qu’il contient.",
+    "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Un SMS a été envoyé à +%(msisdn)s. Saisissez le code de vérification qu’il contient.",
     "Command Help": "Aide aux commandes",
     "No identity server is configured: add one in server settings to reset your password.": "Aucun serveur d’identité n’est configuré : ajoutez-en un dans les paramètres du serveur pour réinitialiser votre mot de passe.",
     "Identity Server URL must be HTTPS": "L’URL du serveur d’identité doit être en HTTPS",
     "Not a valid Identity Server (status code %(code)s)": "Serveur d’identité non valide (code de statut %(code)s)",
     "Could not connect to Identity Server": "Impossible de se connecter au serveur d’identité",
     "Checking server": "Vérification du serveur",
-    "Disconnect from the identity server <idserver />?": "Se déconnecter du serveur d’identité <idserver /> ?",
+    "Disconnect from the identity server <idserver />?": "Se déconnecter du serveur d’identité <idserver /> ?",
     "Disconnect": "Se déconnecter",
     "Identity Server (%(server)s)": "Serveur d’identité (%(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.": "Vous utilisez actuellement <server></server> pour découvrir et être découvert par des contacts existants que vous connaissez. Vous pouvez changer votre serveur d’identité ci-dessous.",
@@ -1487,9 +1487,9 @@
     "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Nous recommandons que vous supprimiez vos adresses e-mail et vos numéros de téléphone du serveur d’identité avant de vous déconnecter.",
     "Disconnect anyway": "Se déconnecter quand même",
     "Error changing power level requirement": "Erreur de changement du critère de rang",
-    "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Une erreur est survenue lors de la modification des critères de rang du salon. Vérifiez que vous avez les bonnes permissions et réessayez.",
+    "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "Une erreur est survenue lors de la modification des critères de rang du salon. Vérifiez que vous avez les permissions nécessaires et réessayez.",
     "No recent messages by %(user)s found": "Aucun message récent de %(user)s n’a été trouvé",
-    "Try scrolling up in the timeline to see if there are any earlier ones.": "Essayez de faire défiler l’historique vers le haut pour voir s’il y en a de plus anciens.",
+    "Try scrolling up in the timeline to see if there are any earlier ones.": "Essayez de faire défiler le fil de discussion vers le haut pour voir s’il y en a de plus anciens.",
     "Remove recent messages by %(user)s": "Supprimer les messages récents de %(user)s",
     "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Vous êtes sur le point de supprimer %(count)s messages de %(user)s. Ça ne peut pas être annulé. Voulez-vous continuer ?",
     "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "Pour un grand nombre de messages, cela peut prendre du temps. N’actualisez pas votre client pendant ce temps.",
@@ -1557,11 +1557,11 @@
     "DuckDuckGo Results": "Résultats de DuckDuckGo",
     "Quick Reactions": "Réactions rapides",
     "Frequently Used": "Utilisé fréquemment",
-    "Smileys & People": "Émoticônes & personnes",
-    "Animals & Nature": "Animaux & nature",
-    "Food & Drink": "Nourriture & boisson",
+    "Smileys & People": "Visages et personnes",
+    "Animals & Nature": "Animaux et nature",
+    "Food & Drink": "Nourriture et boisson",
     "Activities": "Activités",
-    "Travel & Places": "Voyage & lieux",
+    "Travel & Places": "Voyages et lieux",
     "Objects": "Objets",
     "Symbols": "Symboles",
     "Flags": "Drapeaux",
@@ -1608,10 +1608,10 @@
     "User rules": "Règles d’utilisateur",
     "You have not ignored anyone.": "Vous n’avez ignoré personne.",
     "You are currently ignoring:": "Vous ignorez actuellement :",
-    "You are not subscribed to any lists": "Vous n’êtes inscrit(e) à aucune liste",
+    "You are not subscribed to any lists": "Vous n’êtes inscrit à aucune liste",
     "Unsubscribe": "Se désinscrire",
     "View rules": "Voir les règles",
-    "You are currently subscribed to:": "Vous êtes actuellement inscrit(e) à :",
+    "You are currently subscribed to:": "Vous êtes actuellement inscrit à :",
     "⚠ These settings are meant for advanced users.": "⚠ Ces paramètres sont prévus pour les utilisateurs avancés.",
     "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignorer les gens est possible grâce à des listes de bannissement qui contiennent des règles sur les personnes à bannir. L’inscription à une liste de bannissement signifie que les utilisateurs/serveurs bloqués par cette liste seront cachés pour vous.",
     "Personal ban list": "Liste de bannissement personnelle",
@@ -1646,8 +1646,8 @@
     "Connecting to integration manager...": "Connexion au gestionnaire d’intégrations…",
     "Cannot connect to integration manager": "Impossible de se connecter au gestionnaire d’intégrations",
     "The integration manager is offline or it cannot reach your homeserver.": "Le gestionnaire d’intégrations est hors ligne ou il ne peut pas joindre votre serveur d’accueil.",
-    "Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations <b>(%(serverName)s)</b> pour gérer les bots, les widgets et les packs de stickers.",
-    "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations pour gérer les bots, les widgets et les packs de stickers.",
+    "Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations <b>(%(serverName)s)</b> pour gérer les robots, les widgets et les jeux d’autocollants.",
+    "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations pour gérer les robots, les widgets et les jeux d’autocollants.",
     "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Les gestionnaires d’intégrations reçoivent les données de configuration et peuvent modifier les widgets, envoyer des invitations aux salons et définir les rangs à votre place.",
     "Failed to connect to integration manager": "Échec de la connexion au gestionnaire d’intégrations",
     "Widgets do not use message encryption.": "Les widgets n’utilisent pas le chiffrement des messages.",
@@ -1664,8 +1664,8 @@
     "Manage integrations": "Gérer les intégrations",
     "Verification Request": "Demande de vérification",
     "Match system theme": "S’adapter au thème du système",
-    "%(senderName)s placed a voice call.": "%(senderName)s a passé un appel vocal.",
-    "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s a passé un appel vocal. (non pris en charge par ce navigateur)",
+    "%(senderName)s placed a voice call.": "%(senderName)s a passé un appel audio.",
+    "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s a passé un appel audio. (non pris en charge par ce navigateur)",
     "%(senderName)s placed a video call.": "%(senderName)s a passé un appel vidéo.",
     "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s a passé un appel vidéo. (non pris en charge par ce navigateur)",
     "Clear notifications": "Vider les notifications",
@@ -1731,7 +1731,7 @@
     "Close preview": "Fermer l’aperçu",
     "Language Dropdown": "Sélection de la langue",
     "Country Dropdown": "Sélection du pays",
-    "The message you are trying to send is too large.": "Le message que vous essayez d’envoyer est trop gros.",
+    "The message you are trying to send is too large.": "Le message que vous essayez d’envoyer est trop grand.",
     "Help": "Aide",
     "Show more": "En voir plus",
     "Recent Conversations": "Conversations récentes",
@@ -1759,9 +1759,9 @@
     "about a day from now": "dans un jour environ",
     "%(num)s days from now": "dans %(num)s jours",
     "Failed to invite the following users to chat: %(csvUsers)s": "Échec de l’invitation des utilisateurs suivants à discuter : %(csvUsers)s",
-    "We couldn't create your DM. Please check the users you want to invite and try again.": "Impossible de créer votre message direct. Vérifiez les utilisateurs que vous souhaitez inviter et réessayez.",
+    "We couldn't create your DM. Please check the users you want to invite and try again.": "Impossible de créer votre conversation privée. Vérifiez quels utilisateurs que vous souhaitez inviter et réessayez.",
     "Something went wrong trying to invite the users.": "Une erreur est survenue en essayant d’inviter les utilisateurs.",
-    "We couldn't invite those users. Please check the users you want to invite and try again.": "Impossible d’inviter ces utilisateurs. Vérifiez les utilisateurs que vous souhaitez inviter et réessayez.",
+    "We couldn't invite those users. Please check the users you want to invite and try again.": "Impossible d’inviter ces utilisateurs. Vérifiez quels utilisateurs que vous souhaitez inviter et réessayez.",
     "Recently Direct Messaged": "Messages directs récents",
     "Start": "Commencer",
     "Session verified": "Session vérifiée",
@@ -1805,7 +1805,7 @@
     "%(brand)s is securely caching encrypted messages locally for them to appear in search results:": "%(brand)s met en cache les messages chiffrés localement et de manière sécurisée pour qu’ils apparaissent dans les résultats de recherche :",
     "Space used:": "Espace utilisé :",
     "Indexed messages:": "Messages indexés :",
-    "Waiting for %(displayName)s to verify…": "Nous attendons que %(displayName)s vérifie…",
+    "Waiting for %(displayName)s to verify…": "En attente de la vérification de %(displayName)s…",
     "They match": "Ils correspondent",
     "They don't match": "Ils ne correspondent pas",
     "This bridge was provisioned by <user />.": "Cette passerelle a été fournie par <user />.",
@@ -1815,7 +1815,7 @@
     "This room is bridging messages to the following platforms. <a>Learn more.</a>": "Ce salon transmet les messages vers les plateformes suivantes. <a>En savoir plus.</a>",
     "This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "Ce salon ne transmet les messages à aucune plateforme. <a>En savoir plus.</a>",
     "Bridges": "Passerelles",
-    "Waiting for %(displayName)s to accept…": "Nous attendons que %(displayName)s accepte…",
+    "Waiting for %(displayName)s to accept…": "En attente d’acceptation par %(displayName)s…",
     "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Vos messages sont sécurisés et seuls vous et le destinataire avez les clés uniques pour les déchiffrer.",
     "Your messages are not secure": "Vos messages ne sont pas sécurisés",
     "One of the following may be compromised:": "Un des éléments suivants est peut-être compromis :",
@@ -1843,8 +1843,8 @@
     "Never send encrypted messages to unverified sessions from this session": "Ne jamais envoyer de messages chiffrés aux sessions non vérifiées depuis cette session",
     "Never send encrypted messages to unverified sessions in this room from this session": "Ne jamais envoyer des messages chiffrés aux sessions non vérifiées dans ce salon depuis cette session",
     "To be secure, do this in person or use a trusted way to communicate.": "Pour être sûr, faites cela en personne ou utilisez un moyen de communication fiable.",
-    "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.": "Changer votre mot de passe réinitialisera toutes les clés de chiffrement sur toutes les sessions, ce qui rendra l’historique de vos messages illisible, sauf si vous exportez d’abord vos clés de chiffrement et si vous les réimportez ensuite. Dans l’avenir, ce processus sera amélioré.",
-    "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Votre compte à une identité de signature croisée dans le coffre secret, mais cette session ne lui fait pas encore confiance.",
+    "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.": "Changer votre mot de passe réinitialisera toutes les clés de chiffrement sur toutes les sessions, ce qui rendra l’historique de vos messages illisible, sauf si vous exportez d’abord vos clés de chiffrement et si vous les réimportez ensuite. À l’avenir, ce processus sera amélioré.",
+    "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Votre compte a une identité de signature croisée dans le coffre secret, mais cette session ne lui fait pas encore confiance.",
     "in memory": "en mémoire",
     "Your homeserver does not support session management.": "Votre serveur d’accueil ne prend pas en charge la gestion de session.",
     "Unable to load session list": "Impossible de charger la liste de sessions",
@@ -1872,7 +1872,7 @@
     "A session's public name is visible to people you communicate with": "Le nom public d’une session est visible par les personnes avec lesquelles vous communiquez",
     "This user has not verified all of their sessions.": "Cet utilisateur n’a pas vérifié toutes ses sessions.",
     "You have verified this user. This user has verified all of their sessions.": "Vous avez vérifié cet utilisateur. Cet utilisateur a vérifié toutes ses sessions.",
-    "Someone is using an unknown session": "Quelqu'un utilise une session inconnue",
+    "Someone is using an unknown session": "Quelqu’un utilise une session inconnue",
     "Mod": "Modo",
     "Your key share request has been sent - please check your other sessions for key share requests.": "Votre demande de partage de clé a été envoyée − vérifiez les demandes de partage de clé sur vos autres sessions.",
     "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.": "Les demandes de partage de clé sont envoyées à vos autres sessions automatiquement. Si vous avez rejeté ou ignoré la demande de partage de clé sur vos autres sessions, cliquez ici pour redemander les clés pour cette session.",
@@ -1894,7 +1894,7 @@
     "Session name": "Nom de la session",
     "Session key": "Clé de la session",
     "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Vérifier cet utilisateur marquera sa session comme fiable, et marquera aussi votre session comme fiable pour lui.",
-    "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Vérifier cet appareil le marquera comme fiable. Faire confiance à cette appareil vous permettra à vous et aux autres utilisateurs d’être sereins lors de l’utilisation de messages chiffrés.",
+    "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Vérifier cet appareil le marquera comme fiable. Faire confiance à cette appareil vous permettra à vous et aux autres utilisateurs d’être tranquilles lors de l’utilisation de messages chiffrés.",
     "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Vérifier cet appareil le marquera comme fiable, et les utilisateurs qui ont vérifié avec vous feront confiance à cet appareil.",
     "This will allow you to return to your account after signing out, and sign in on other sessions.": "Cela vous permettra de revenir sur votre compte après vous être déconnecté, et de vous connecter sur d’autres sessions.",
     "Without completing security on this session, it won’t have access to encrypted messages.": "Sans compléter la sécurité sur cette session, elle n’aura pas accès aux messages chiffrés.",
@@ -1954,10 +1954,10 @@
     "Accepting…": "Acceptation…",
     "Accepting …": "Acceptation…",
     "Declining …": "Refus…",
-    "Your account is not secure": "Votre compte n'est pas sécurisé",
+    "Your account is not secure": "Votre compte n’est pas sûr",
     "Your password": "Votre mot de passe",
-    "This session, or the other session": "Cette session, ou l'autre session",
-    "The internet connection either session is using": "La connection internet de l'une des sessions",
+    "This session, or the other session": "Cette session, ou l’autre session",
+    "The internet connection either session is using": "La connexion internet de l’une des sessions",
     "We recommend you change your password and recovery key in Settings immediately": "Nous vous recommandons de changer votre mot de passe et la clé de récupération dans Paramètres dès que possible",
     "Sign In or Create Account": "Se connecter ou créer un compte",
     "Use your account or create a new one to continue.": "Utilisez votre compte ou créez en un pour continuer.",
@@ -1981,7 +1981,7 @@
     "%(senderName)s changed the addresses for this room.": "%(senderName)s a changé les adresses de ce salon.",
     "Support adding custom themes": "Autoriser l’ajout de thèmes personnalisés",
     "Invalid theme schema.": "Schéma du thème invalide.",
-    "Error downloading theme information.": "Une erreur s'est produite en téléchargeant les informations du thème.",
+    "Error downloading theme information.": "Une erreur s’est produite en téléchargeant les informations du thème.",
     "Theme added!": "Thème ajouté !",
     "Custom theme URL": "URL personnalisée pour le thème",
     "Add theme": "Ajouter le thème",
@@ -1989,7 +1989,7 @@
     "Local address": "Adresse locale",
     "Published Addresses": "Adresses publiées",
     "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.": "Les adresses publiées peuvent être utilisées par n’importe qui sur n’importe quel serveur pour rejoindre votre salon. Pour publier une adresse, elle doit d’abord être définie comme adresse locale.",
-    "Other published addresses:": "Autres adresses publiques :",
+    "Other published addresses:": "Autres adresses publiées :",
     "No other published addresses yet, add one below": "Aucune autre adresse n’est publiée, ajoutez-en une ci-dessous",
     "New published address (e.g. #alias:server)": "Nouvelles adresses publiées (par ex. #alias:serveur)",
     "Local Addresses": "Adresses locales",
@@ -2032,9 +2032,9 @@
     "Toggle Italics": "(Dés)activer l’italique",
     "Toggle Quote": "(Dés)activer la citation",
     "New line": "Nouvelle ligne",
-    "Navigate recent messages to edit": "Parcourir les messages récents pour éditer",
+    "Navigate recent messages to edit": "Parcourir les messages récents pour modifier",
     "Jump to start/end of the composer": "Sauter au début/à la fin du compositeur",
-    "Toggle microphone mute": "(Dés)activer la sourdine du micro",
+    "Toggle microphone mute": "Activer/désactiver le micro",
     "Toggle video on/off": "Dés(activer) la vidéo",
     "Jump to room search": "Sauter à la recherche de salon",
     "Navigate up/down in the room list": "Parcourir avec haut/bas dans la liste des salons",
@@ -2042,7 +2042,7 @@
     "Collapse room list section": "Réduire la section de la liste des salons",
     "Expand room list section": "Développer la section de la liste des salons",
     "Clear room list filter field": "Effacer le champ de filtrage de la liste des salons",
-    "Scroll up/down in the timeline": "Défiler vers le haut/le bas dans l’historique",
+    "Scroll up/down in the timeline": "Défiler vers le haut/le bas dans le fil de discussion",
     "Toggle the top left menu": "Afficher/masquer le menu en haut à gauche",
     "Close dialog or context menu": "Fermer le dialogue ou le menu contextuel",
     "Activate selected button": "Activer le bouton sélectionné",
@@ -2061,8 +2061,8 @@
     "Confirm this user's session by comparing the following with their User Settings:": "Confirmez la session de cet utilisateur en comparant ceci avec ses paramètres utilisateur :",
     "If they don't match, the security of your communication may be compromised.": "S’ils ne correspondent pas, la sécurité de vos communications est peut-être compromise.",
     "Navigate composer history": "Explorer l’historique du compositeur",
-    "Previous/next unread room or DM": "Salon ou message direct non lu précédent/suivant",
-    "Previous/next room or DM": "Salon ou message direct précédent/suivant",
+    "Previous/next unread room or DM": "Salon ou conversation privée non lu précédent/suivant",
+    "Previous/next room or DM": "Salon ou conversation privée précédent/suivant",
     "Toggle right panel": "Afficher/masquer le panneau de droite",
     "Manually verify all remote sessions": "Vérifier manuellement toutes les sessions à distance",
     "Self signing private key:": "Clé privée d’auto-signature :",
@@ -2094,7 +2094,7 @@
     "Delete sessions": "Supprimer les sessions",
     "Confirm the emoji below are displayed on both sessions, in the same order:": "Confirmez que les émojis ci-dessous s’affichent dans les deux sessions et dans le même ordre :",
     "Verify this session by confirming the following number appears on its screen.": "Vérifiez cette session en confirmant que le nombre suivant s’affiche sur son écran.",
-    "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "Dans l’attente que votre autre session, %(deviceName)s (%(deviceId)s), vérifie…",
+    "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "En attente de la vérification depuis votre autre session, %(deviceName)s (%(deviceId)s)…",
     "From %(deviceName)s (%(deviceId)s)": "Depuis %(deviceName)s (%(deviceId)s)",
     "Waiting for you to accept on your other session…": "Dans l’attente que vous acceptiez dans votre autre session…",
     "Almost there! Is your other session showing the same shield?": "On y est presque ! Est-ce que votre autre session affiche le même bouclier ?",
@@ -2128,7 +2128,7 @@
     "Delete sessions|other": "Supprimer les sessions",
     "Delete sessions|one": "Supprimer la session",
     "Enable end-to-end encryption": "Activer le chiffrement de bout en bout",
-    "You can’t disable this later. Bridges & most bots won’t work yet.": "Vous ne pourrez pas le désactiver plus tard. Les passerelles et la plupart des bots ne fonctionneront pas pour le moment.",
+    "You can’t disable this later. Bridges & most bots won’t work yet.": "Vous ne pourrez pas le désactiver plus tard. Les passerelles et la plupart des robots ne fonctionneront pas pour le moment.",
     "Failed to set topic": "Échec du changement de sujet",
     "Command failed": "La commande a échoué",
     "Could not find user in room": "Impossible de trouver l’utilisateur dans le salon",
@@ -2151,7 +2151,7 @@
     "Secure your backup with a recovery passphrase": "Protégez votre sauvegarde avec une phrase secrète de récupération",
     "Can't load this message": "Impossible de charger ce message",
     "Submit logs": "Envoyer les journaux",
-    "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "Rappel : Votre navigateur n’est pas pris en charge donc votre expérience pourrait être imprévisible.",
+    "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "Rappel : Votre navigateur n’est pas pris en charge donc votre expérience pourrait être aléatoire.",
     "Unable to upload": "Envoi impossible",
     "Currently indexing: %(currentRoom)s": "En train d’indexer : %(currentRoom)s",
     "Please supply a widget URL or embed code": "Veuillez fournir l’URL ou le code d’intégration du widget",
@@ -2164,15 +2164,15 @@
     "Where you’re logged in": "Où vous vous êtes connecté",
     "Manage the names of and sign out of your sessions below or <a>verify them in your User Profile</a>.": "Gérez les noms et déconnectez-vous de vos sessions ci-dessous ou <a>vérifiez-les dans votre profil utilisateur</a>.",
     "Review where you’re logged in": "Vérifier où vous vous êtes connecté",
-    "New login. Was this you?": "Nouvelle connexion. Était-ce vous ?",
-    "Verify all your sessions to ensure your account & messages are safe": "Vérifiez toutes vos sessions pour vous assurer que votre compte et messages sont sécurisés",
+    "New login. Was this you?": "Nouvelle connexion. Était-ce vous ?",
+    "Verify all your sessions to ensure your account & messages are safe": "Vérifiez toutes vos sessions pour vous assurer que votre compte et messages ne sont pas compromis",
     "Verify the new login accessing your account: %(name)s": "Vérifiez la nouvelle connexion accédant à votre compte : %(name)s",
     "Restoring keys from backup": "Restauration des clés depuis la sauvegarde",
     "Fetching keys from server...": "Récupération des clés depuis le serveur…",
     "%(completed)s of %(total)s keys restored": "%(completed)s clés sur %(total)s restaurées",
     "Keys restored": "Clés restaurées",
     "Successfully restored %(sessionCount)s keys": "%(sessionCount)s clés ont été restaurées avec succès",
-    "You signed in to a new session without verifying it:": "Vous vous êtes connecté·e à une nouvelle session sans la vérifier :",
+    "You signed in to a new session without verifying it:": "Vous vous êtes connecté à une nouvelle session sans la vérifier :",
     "Verify your other session using one of the options below.": "Vérifiez votre autre session en utilisant une des options ci-dessous.",
     "Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.": "Invitez quelqu’un en utilisant leur nom, leur nom d’utilisateur (comme <userId/>), leur adresse e-mail ou <a>partagez ce salon</a>.",
     "Message deleted": "Message supprimé",
@@ -2188,9 +2188,9 @@
     "Click the button below to confirm setting up encryption.": "Cliquez sur le bouton ci-dessous pour confirmer la configuration du chiffrement.",
     "QR Code": "Code QR",
     "Dismiss read marker and jump to bottom": "Ignorer le signet de lecture et aller en bas",
-    "Jump to oldest unread message": "Aller au plus vieux message non lu",
+    "Jump to oldest unread message": "Aller au plus ancien message non lu",
     "Upload a file": "Envoyer un fichier",
-    "IRC display name width": "Largeur du nom affiché IRC",
+    "IRC display name width": "Largeur du nom d’affichage IRC",
     "Create room": "Créer un salon",
     "Font scaling": "Mise à l’échelle de la police",
     "Font size": "Taille de la police",
@@ -2239,7 +2239,7 @@
     "Switch to light mode": "Passer au mode clair",
     "Switch to dark mode": "Passer au mode sombre",
     "Switch theme": "Changer le thème",
-    "Security & privacy": "Sécurité & vie privée",
+    "Security & privacy": "Sécurité et vie privée",
     "All settings": "Tous les paramètres",
     "Feedback": "Commentaire",
     "No recently visited rooms": "Aucun salon visité récemment",
@@ -2278,8 +2278,8 @@
     "You left the call": "Vous avez quitté l’appel",
     "%(senderName)s left the call": "%(senderName)s a quitté l’appel",
     "Call ended": "Appel terminé",
-    "You started a call": "Vous avez démarré un appel",
-    "%(senderName)s started a call": "%(senderName)s a démarré un appel",
+    "You started a call": "Vous avez commencé un appel",
+    "%(senderName)s started a call": "%(senderName)s a commencé un appel",
     "Waiting for answer": "En attente d’une réponse",
     "%(senderName)s is calling": "%(senderName)s appelle",
     "You created the room": "Vous avez créé le salon",
@@ -2337,10 +2337,10 @@
     "Use your Security Key to continue.": "Utilisez votre clé de sécurité pour continuer.",
     "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Protection afin d’éviter de perdre l’accès aux messages et données chiffrés en sauvegardant les clés de chiffrement sur votre serveur.",
     "Generate a Security Key": "Générer une clé de sécurité",
-    "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Nous génèrerons une clé de sécurité que vous devrez stocker dans un endroit sûr, comme un gestionnaire de mots de passe ou un coffre.",
+    "We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.": "Nous générerons une clé de sécurité que vous devrez stocker dans un endroit sûr, comme un gestionnaire de mots de passe ou un coffre.",
     "Enter a Security Phrase": "Saisir une phrase de sécurité",
-    "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Utilisez une phrase secrète que vous êtes seul·e à connaître et enregistrez éventuellement une clé de sécurité à utiliser pour la sauvegarde.",
-    "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.": "Saisissez une phrase de sécurité que vous seul·e connaissez, car elle est utilisée pour protéger vos données. Pour plus de sécurité, vous ne devriez pas réutiliser le mot de passe de votre compte.",
+    "Use a secret phrase only you know, and optionally save a Security Key to use for backup.": "Utilisez une phrase secrète que vous êtes seul à connaître et enregistrez éventuellement une clé de sécurité à utiliser pour la sauvegarde.",
+    "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.": "Saisissez une phrase de sécurité que vous seul connaissez, car elle est utilisée pour protéger vos données. Pour plus de sécurité, vous ne devriez pas réutiliser le mot de passe de votre compte.",
     "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Stockez votre clé de sécurité dans un endroit sûr, comme un gestionnaire de mots de passe ou un coffre, car elle est utilisée pour protéger vos données chiffrées.",
     "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "Si vous annulez maintenant, vous pourriez perdre vos messages et données chiffrés si vous perdez l’accès à vos identifiants.",
     "You can also set up Secure Backup & manage your keys in Settings.": "Vous pouvez aussi configurer la sauvegarde sécurisée et gérer vos clés depuis les paramètres.",
@@ -2353,7 +2353,7 @@
     "Riot is now Element!": "Riot est maintenant Element !",
     "Learn More": "Plus d'infos",
     "Enable experimental, compact IRC style layout": "Disposition expérimentale compacte style IRC",
-    "Incoming voice call": "Appel vocal entrant",
+    "Incoming voice call": "Appel audio entrant",
     "Incoming video call": "Appel vidéo entrant",
     "Incoming call": "Appel entrant",
     "Make this room low priority": "Définir ce salon en priorité basse",
@@ -2367,13 +2367,13 @@
     "Favourited": "Favori",
     "Forget Room": "Oublier le salon",
     "This room is public": "Ce salon est public",
-    "Edited at %(date)s": "Édité le %(date)s",
-    "Click to view edits": "Cliquez pour éditer",
+    "Edited at %(date)s": "Modifié le %(date)s",
+    "Click to view edits": "Cliquez pour voir les modifications",
     "Go to Element": "Aller à Element",
     "We’re excited to announce Riot is now Element!": "Nous sommes heureux d'annoncer que Riot est désormais Element !",
     "Learn more at <a>element.io/previously-riot</a>": "Plus d'infos sur <a>element.io/previously-riot</a>",
     "Search rooms": "Chercher des salons",
-    "User menu": "Menu d'utilisateur",
+    "User menu": "Menu d’utilisateur",
     "%(brand)s Web": "%(brand)s Web",
     "%(brand)s Desktop": "%(brand)s Desktop",
     "%(brand)s iOS": "%(brand)s iOS",
@@ -2385,20 +2385,20 @@
     "The person who invited you already left the room, or their server is offline.": "La personne vous ayant invité a déjà quitté le salon, ou son serveur est hors-ligne.",
     "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s",
     "Change notification settings": "Modifier les paramètres de notification",
-    "Show message previews for reactions in DMs": "Afficher la prévisualisation des messages pour les réactions dans les messages privés",
+    "Show message previews for reactions in DMs": "Afficher l’aperçu des messages pour les réactions dans les conversations privées",
     "Show message previews for reactions in all rooms": "Afficher la prévisualisation des messages pour les réactions dans tous les salons",
     "Enable advanced debugging for the room list": "Activer le débogage avancé pour la liste de salons",
-    "Uploading logs": "Téléversement des journaux",
+    "Uploading logs": "Envoi des journaux",
     "Downloading logs": "Téléchargement des journaux",
     "Your server isn't responding to some <a>requests</a>.": "Votre serveur ne répond pas à certaines <a>requêtes</a>.",
     "Cross-signing and secret storage are ready for use.": "La signature croisée et le coffre secret sont prêt à l'emploi.",
     "Cross-signing is ready for use, but secret storage is currently not being used to backup your keys.": "La signature croisée est prête à l'emploi, mais le coffre secret n'est pas actuellement utilisé pour sauvegarder vos clés.",
-    "Master private key:": "Clé privée maîtresse :",
+    "Master private key:": "Clé privée maîtresse :",
     "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s ne peut actuellement mettre en cache vos messages chiffrés localement de manière sécurisée via le navigateur Web. Utilisez <desktopLink>%(brand)s Desktop</desktopLink> pour que les messages chiffrés apparaissent dans vos résultats de recherche.",
     "There are advanced notifications which are not shown here.": "Des notifications avancées ne sont pas affichées ici.",
     "ready": "prêt",
-    "The operation could not be completed": "L'opération n'a pas pu être terminée",
-    "Failed to save your profile": "Erreur lors de l'enregistrement du profile",
+    "The operation could not be completed": "L’opération n’a pas pu être terminée",
+    "Failed to save your profile": "Erreur lors de l’enregistrement du profil",
     "Unknown App": "Application inconnue",
     "%(senderName)s declined the call.": "%(senderName)s a refusé l’appel.",
     "(an error occurred)": "(une erreur est survenue)",
@@ -2408,39 +2408,39 @@
     "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Ajoute ( ͡° ͜ʖ ͡°) en préfixe du message",
     "This will end the conference for everyone. Continue?": "Ceci arrêtera la téléconférence pour tout le monde. Continuer ?",
     "End conference": "Finir la téléconférence",
-    "The call was answered on another device.": "L’appel a été répondu sur un autre appareil.",
+    "The call was answered on another device.": "L’appel a été décroché sur un autre appareil.",
     "Answered Elsewhere": "Répondu autre-part",
     "The call could not be established": "L’appel n’a pas pu être établi",
     "The other party declined the call.": "Le correspondant a décliné l’appel.",
     "Call Declined": "Appel rejeté",
     "Ignored attempt to disable encryption": "Essai de désactiver le chiffrement ignoré",
-    "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Ajoutez les utilisateurs et les serveurs que vous voulez ignorer ici. Utilisez des astérisques pour que %(brand)s comprenne tous les caractères. Par exemple, <code>@bot:*</code> va ignorer tous les utilisateurs ayant le nom 'bot' sur n'importe quel serveur.",
+    "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, <code>@bot:*</code> would ignore all users that have the name 'bot' on any server.": "Ajoutez les utilisateurs et les serveurs que vous voulez ignorer ici. Utilisez des astérisques pour que %(brand)s comprenne tous les caractères. Par exemple, <code>@bot:*</code> va ignorer tous les utilisateurs ayant le nom « bot » sur n’importe quel serveur.",
     "not ready": "pas prêt",
-    "Secret storage:": "Coffre secret :",
-    "Backup key cached:": "Clé de sauvegarde mise en cache :",
-    "Backup key stored:": "Clé de sauvegarde enregistrée :",
+    "Secret storage:": "Coffre secret :",
+    "Backup key cached:": "Clé de sauvegarde mise en cache :",
+    "Backup key stored:": "Clé de sauvegarde enregistrée :",
     "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Sauvegarder vos clés de chiffrement avec les données de votre compte dans le cas où vous perdez l'accès à vos sessions. Vos clés seront sécurisées grâce à une unique clé de récupération.",
-    "Backup version:": "Version de la sauvegarde :",
-    "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Vous les avez peut-être configurés dans un autre client que %(brand)s. Vous ne pouvez pas les configurer dans %(brand)s mais ils s'appliquent quand même.",
+    "Backup version:": "Version de la sauvegarde :",
+    "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "Vous les avez peut-être configurés dans un autre client que %(brand)s. Vous ne pouvez pas les configurer dans %(brand)s mais ils s’appliquent quand même.",
     "not found in storage": "non trouvé dans le coffre",
-    "Cross-signing is not set up.": "La signature croisée n'est pas configurée.",
+    "Cross-signing is not set up.": "La signature croisée n’est pas configurée.",
     "Cross-signing is ready for use.": "La signature croisée est prête à être utilisée.",
     "Offline encrypted messaging using dehydrated devices": "Messagerie hors-ligne chiffrée utilisant des appareils déshydratés",
-    "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Prototype de communautés v2. Requiert un serveur d'accueil compatible. Très expérimental - à utiliser avec précaution.",
-    "Safeguard against losing access to encrypted messages & data": "Sécurité contre la perte d'accès aux messages & données chiffrées",
+    "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Prototype de communautés v2. Requiert un serveur d’accueil compatible. Très expérimental - à utiliser avec précaution.",
+    "Safeguard against losing access to encrypted messages & data": "Sécurité contre la perte d’accès aux messages et données chiffrées",
     "(their device couldn't start the camera / microphone)": "(leur appareil ne peut pas démarrer la caméra/le microphone)",
     "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Tous les serveurs ont été bannis ! Ce salon ne peut plus être utilisé.",
     "What's the name of your community or team?": "Quel est le nom de votre communauté ou équipe ?",
     "You can change this later if needed.": "Vous pouvez modifier ceci après si besoin.",
-    "Use this when referencing your community to others. The community ID cannot be changed.": "Utilisez ceci lorsque vous faites référence à votre communauté aux autres. L'identifiant de la communauté ne peut pas être modifié.",
+    "Use this when referencing your community to others. The community ID cannot be changed.": "Utilisez ceci lorsque vous faites référence à votre communauté aux autres. L’identifiant de la communauté ne peut pas être modifié.",
     "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Une erreur est survenue lors de la création de votre communauté. Le nom est peut-être pris ou le serveur ne peut pas exécuter votre requête.",
     "This version of %(brand)s does not support searching encrypted messages": "Cette version de %(brand)s ne prend pas en charge la recherche dans les messages chiffrés",
-    "This version of %(brand)s does not support viewing some encrypted files": "Cette version de %(brand)s ne prend pas en charge l'affichage de certains fichiers chiffrés",
+    "This version of %(brand)s does not support viewing some encrypted files": "Cette version de %(brand)s ne prend pas en charge l’affichage de certains fichiers chiffrés",
     "Use the <a>Desktop app</a> to search encrypted messages": "Utilisez une <a>Application de bureau</a> pour rechercher dans tous les messages chiffrés",
     "Use the <a>Desktop app</a> to see all encrypted files": "Utilisez une <a>Application de bureau</a> pour voir tous les fichiers chiffrés",
     "Join the conference at the top of this room": "Rejoignez la téléconférence en haut de ce salon",
-    "Join the conference from the room information card on the right": "Rejoignez la téléconférence à partir de la carte d'informations sur la droite",
-    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Définissez le nom d'une police de caractères installée sur votre système et %(brand)s essaiera de l'utiliser.",
+    "Join the conference from the room information card on the right": "Rejoignez la téléconférence à partir de la carte d’informations sur la droite",
+    "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Définissez le nom d’une police de caractères installée sur votre système et %(brand)s essaiera de l’utiliser.",
     "Create a room in %(communityName)s": "Créer un salon dans %(communityName)s",
     "An image will help people identify your community.": "Une image aidera les personnes à identifier votre communauté.",
     "Add image (optional)": "Ajouter une image (facultatif)",
@@ -2463,32 +2463,32 @@
     "%(count)s people|one": "%(count)s personne",
     "About": "À propos",
     "Not encrypted": "Non-chiffré",
-    "Add widgets, bridges & bots": "Ajouter des widgets, ponts et bots",
-    "Edit widgets, bridges & bots": "Modifier les widgets, ponts et bots",
+    "Add widgets, bridges & bots": "Ajouter des widgets, passerelles et robots",
+    "Edit widgets, bridges & bots": "Modifier les widgets, passerelles et robots",
     "Widgets": "Widgets",
-    "Unpin a widget to view it in this panel": "Dépingler un widget pour l'afficher dans ce panneau",
+    "Unpin a widget to view it in this panel": "Dépingler un widget pour l’afficher dans ce panneau",
     "Unpin": "Dépingler",
-    "You can only pin up to %(count)s widgets|other": "Vous ne pouvez épingler que jusqu'à %(count)s widgets",
+    "You can only pin up to %(count)s widgets|other": "Vous ne pouvez épingler que jusqu’à %(count)s widgets",
     "Room Info": "Informations sur le salon",
     "%(count)s results|one": "%(count)s résultat",
     "%(count)s results|other": "%(count)s résultats",
-    "Explore all public rooms": "Explorer tous les salons publiques",
-    "Can't see what you’re looking for?": "Vous ne voyez pas ce que vous cherchez ?",
+    "Explore all public rooms": "Explorer tous les salons publics",
+    "Can't see what you’re looking for?": "Vous ne trouvez pas ce que vous cherchez ?",
     "Custom Tag": "Étiquette personnalisée",
-    "Explore public rooms": "Explorer les salons publiques",
+    "Explore public rooms": "Explorer les salons publics",
     "Explore community rooms": "Explorer les salons de la communauté",
     "Show Widgets": "Afficher les widgets",
     "Hide Widgets": "Masquer les widgets",
-    "Remove messages sent by others": "Supprimer les messages envoyés par d'autres",
+    "Remove messages sent by others": "Supprimer les messages envoyés par d’autres",
     "Privacy": "Vie privée",
     "Secure Backup": "Sauvegarde sécurisée",
     "Algorithm:": "Algorithme :",
     "Set up Secure Backup": "Configurer la sauvegarde sécurisée",
     "%(brand)s Android": "%(brand)s Android",
-    "Community and user menu": "Menu de la communauté et de l'utilisateur",
-    "User settings": "Paramètres de l'utilisateur",
+    "Community and user menu": "Menu de la communauté et de l’utilisateur",
+    "User settings": "Paramètres de l’utilisateur",
     "Community settings": "Paramètres de la communauté",
-    "Failed to find the general chat for this community": "Impossible de trouver le chat général de cette communauté",
+    "Failed to find the general chat for this community": "Impossible de trouver la discussion générale de cette communauté",
     "Starting microphone...": "Allumage du microphone …",
     "Starting camera...": "Allumage de la caméra ...",
     "Call connecting...": "Connexion à l'appel …",
@@ -2496,10 +2496,10 @@
     "Explore rooms in %(communityName)s": "Explorer les salons dans %(communityName)s",
     "You have no visible notifications in this room.": "Vous n'avez pas de notification visible dans ce salon.",
     "You’re all caught up": "Vous êtes à jour",
-    "You do not have permission to create rooms in this community.": "Vous n'avez pas la permission de créer des salons dans cette communauté.",
+    "You do not have permission to create rooms in this community.": "Vous n’avez pas la permission de créer des salons dans cette communauté.",
     "Cannot create rooms in this community": "Impossible de créer des salons dans cette communauté",
     "Create community": "Créer une communauté",
-    "Attach files from chat or just drag and drop them anywhere in a room.": "Envoyez des fichiers depuis le chat ou juste glissez et déposez-les n'importe où dans le salon.",
+    "Attach files from chat or just drag and drop them anywhere in a room.": "Envoyez des fichiers depuis la discussion ou glissez et déposez-les n’importe où dans le salon.",
     "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.",
@@ -2509,61 +2509,61 @@
     "Revoke permissions": "Révoquer les permissions",
     "Take a picture": "Prendre une photo",
     "Unable to set up keys": "Impossible de configurer les clés",
-    "Recent changes that have not yet been received": "Changements récents qui n'ont pas encore été reçus",
-    "The server is not configured to indicate what the problem is (CORS).": "Le n'est n'est pas configuré pour indiquer quel est le problème (CORS).",
+    "Recent changes that have not yet been received": "Changements récents qui n’ont pas encore été reçus",
+    "The server is not configured to indicate what the problem is (CORS).": "Le serveur n’est pas configuré pour indiquer quel est le problème (CORS).",
     "A connection error occurred while trying to contact the server.": "Une erreur de connexion est survenue en essayant de contacter le serveur.",
-    "Your area is experiencing difficulties connecting to the internet.": "Votre emplacement connaît des difficultés à se connecter à Internet.",
+    "Your area is experiencing difficulties connecting to the internet.": "Votre secteur connaît des difficultés à se connecter à Internet.",
     "The server has denied your request.": "Le serveur a refusé votre requête.",
     "The server is offline.": "Le serveur est éteint.",
     "A browser extension is preventing the request.": "Une extension du navigateur bloque la requête.",
     "Your firewall or anti-virus is blocking the request.": "Votre pare-feu ou votre antivirus bloque la requête.",
     "The server (%(serverName)s) took too long to respond.": "Le serveur (%(serverName)s) met trop de temps à répondre.",
-    "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Votre serveur ne répond pas à certaines requêtes. Vous trouverez ci-dessus quelles sont la plupart des raisons.",
+    "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Votre serveur ne répond pas à certaines requêtes. Vous trouverez ci-dessus quelles en sont les raisons probables.",
     "Server isn't responding": "Le serveur ne répond pas",
     "You're all caught up.": "Vous êtes à jour.",
     "Data on this screen is shared with %(widgetDomain)s": "Les données sur cet écran sont partagées avec %(widgetDomain)s",
     "Modal Widget": "Fenêtre de widget",
-    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invitez quelqu'un à partir de son nom, pseudo (comme <userId/>) ou <a>partagez ce salon</a>.",
-    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Cela ne va pas l'inviter à %(communityName)s. Pour inviter quelqu'un à %(communityName)s, cliquez <a>ici</a>",
-    "Start a conversation with someone using their name or username (like <userId/>).": "Démarrer une conversation avec quelqu'un en utilisant son nom ou son pseudo (comme <userId/>).",
+    "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invitez quelqu’un à partir de son nom, pseudo (comme <userId/>) ou <a>partagez ce salon</a>.",
+    "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "Cela ne va pas l’inviter à %(communityName)s. Pour inviter quelqu’un à %(communityName)s, cliquez <a>ici</a>",
+    "Start a conversation with someone using their name or username (like <userId/>).": "Commencer une conversation privée avec quelqu’un en utilisant son nom ou son pseudo (comme <userId/>).",
     "May include members not in %(communityName)s": "Peut inclure des membres qui ne sont pas dans %(communityName)s",
     "Send feedback": "Envoyer un commentaire",
-    "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "CONSEIL : si vous reportez un bug, merci d'envoyer <debugLogsLink>les logs de débogage</debugLogsLink> pour nous aider à identifier le problème.",
-    "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Merci de regarder d'abord les <existingIssuesLink>bugs déjà répertoriés sur Github</existingIssuesLink>. Pas de résultat ? <newIssueLink>Reportez un nouveau bug</newIssueLink>.",
-    "Report a bug": "Reporter un bug",
+    "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "CONSEIL : si vous rapportez un bug, merci d’envoyer <debugLogsLink>les journaux de débogage</debugLogsLink> pour nous aider à identifier le problème.",
+    "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Merci de regarder d’abord les <existingIssuesLink>bugs déjà répertoriés sur Github</existingIssuesLink>. Pas de résultat ? <newIssueLink>Rapportez un nouveau bug</newIssueLink>.",
+    "Report a bug": "Rapporter un bug",
     "There are two ways you can provide feedback and help us improve %(brand)s.": "Il y a deux manières pour que vous puissiez faire vos retour et nous aider à améliorer %(brand)s.",
     "Comment": "Commentaire",
     "Add comment": "Ajouter un commentaire",
-    "Please go into as much detail as you like, so we can track down the problem.": "Merci d'ajouter le plus de détails possible, pour que nous puissions mieux identifier le problème.",
+    "Please go into as much detail as you like, so we can track down the problem.": "Merci d’ajouter le plus de détails possible, pour que nous puissions mieux identifier le problème.",
     "Tell us below how you feel about %(brand)s so far.": "Dites-nous ci-dessous quel est votre ressenti à propos de %(brand)s jusque là.",
     "Rate %(brand)s": "Noter %(brand)s",
     "Feedback sent": "Commentaire envoyé",
     "Update community": "Modifier la communauté",
     "There was an error updating your community. The server is unable to process your request.": "Une erreur est survenue lors de la mise à jour de votre communauté. Le serveur est incapable de traiter votre requête.",
-    "Block anyone not part of %(serverName)s from ever joining this room.": "Bloque n'importe qui qui n'est pas membre de %(serverName)s de rejoindre ce salon.",
-    "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Vous devriez le déactiver si le salon est utilisé pour collaborer avec des équipes externes qui ont leur propre serveur d'accueil. Ce ne peut pas être changé plus tard.",
-    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Vous devriez l'activer si le salon n'est utilisé que pour collaborer avec des équipes internes sur votre serveur d'accueil. Ce ne peut pas être changé plus tard.",
-    "Your server requires encryption to be enabled in private rooms.": "Votre serveur requiert d'activer le chiffrement dans les salons privés.",
-    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Les salons privés ne peuvent être trouvés et rejoints seulement par invitation. Les salons publics peut être trouvés et rejoints par n'importe qui dans cette communauté.",
-    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Les salons privés ne peuvent être trouvés et rejoints seulement par invitation. Les salons publics peut être trouvés et rejoints par n'importe qui.",
-    "Start a new chat": "Commencer une nouvelle discussion",
+    "Block anyone not part of %(serverName)s from ever joining this room.": "Empêche n’importe qui n’étant pas membre de %(serverName)s de rejoindre ce salon.",
+    "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "Vous devriez le déactiver si le salon est utilisé pour collaborer avec des équipes externes qui ont leur propre serveur d’accueil. Ceci ne peut pas être changé plus tard.",
+    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "Vous devriez l’activer si le salon n’est utilisé que pour collaborer avec des équipes internes sur votre serveur d’accueil. Ceci ne peut pas être changé plus tard.",
+    "Your server requires encryption to be enabled in private rooms.": "Votre serveur impose d’activer le chiffrement dans les salons privés.",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Les salons privés ne peuvent être trouvés et rejoints seulement par invitation. Les salons publics peut être trouvés et rejoints par n’importe qui dans cette communauté.",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Les salons privés ne peuvent être trouvés et rejoints seulement par invitation. Les salons publics peut être trouvés et rejoints par n’importe qui.",
+    "Start a new chat": "Commencer une nouvelle conversation privée",
     "Add a photo so people know it's you.": "Ajoutez une photo pour que les gens sachent qu’il s’agit de vous.",
     "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s ou %(usernamePassword)s",
     "Decide where your account is hosted": "Décidez où votre compte est hébergé",
-    "Go to Home View": "Revenir à la page d'accueil",
+    "Go to Home View": "Revenir à la page d’accueil",
     "Use Ctrl + Enter to send a message": "Utilisez Ctrl + Entrée pour envoyer un message",
-    "%(senderName)s ended the call": "%(senderName)s a terminé l'appel",
-    "You ended the call": "Vous avez terminé l'appel",
-    "%(creator)s created this DM.": "%(creator)s a envoyé ce DM.",
-    "Now, let's help you get started": "Maintenant, commençons à vous initier",
+    "%(senderName)s ended the call": "%(senderName)s a terminé l’appel",
+    "You ended the call": "Vous avez terminé l’appel",
+    "%(creator)s created this DM.": "%(creator)s a créé cette conversation privée.",
+    "Now, let's help you get started": "Maintenant, initions vous",
     "Welcome %(name)s": "Bienvenue %(name)s",
     "Filter rooms and people": "Filtrer des salons et personnes",
-    "Got an account? <a>Sign in</a>": "Vous avez un compte ? <a>Connectez-vous</a>",
-    "New here? <a>Create an account</a>": "Nouveau ici ? <a>Créez un compte</a>",
-    "There was a problem communicating with the homeserver, please try again later.": "Il y a eu un problème lors de la communication avec le serveur d'accueil, veuillez réessayer ultérieurement.",
-    "New? <a>Create account</a>": "Nouveau ? <a>Créez un compte</a>",
-    "That username already exists, please try another.": "Ce nom d'utilisateur existe déjà, essayez-en un autre.",
-    "Already have an account? <a>Sign in here</a>": "Vous avez déjà un compte ? <a>Connectez-vous ici</a>",
+    "Got an account? <a>Sign in</a>": "Vous avez un compte ? <a>Connectez-vous</a>",
+    "New here? <a>Create an account</a>": "Nouveau ici ? <a>Créez un compte</a>",
+    "There was a problem communicating with the homeserver, please try again later.": "Il y a eu un problème lors de la communication avec le serveur d’accueil, veuillez réessayer ultérieurement.",
+    "New? <a>Create account</a>": "Nouveau ? <a>Créez un compte</a>",
+    "That username already exists, please try another.": "Ce nom d’utilisateur existe déjà, essayez-en un autre.",
+    "Already have an account? <a>Sign in here</a>": "Vous avez déjà un compte ? <a>Connectez-vous ici</a>",
     "Algeria": "Algérie",
     "Albania": "Albanie",
     "Åland Islands": "Îles Åland",
@@ -2572,7 +2572,7 @@
     "United Kingdom": "Royaume-Uni",
     "You've reached the maximum number of simultaneous calls.": "Vous avez atteint le nombre maximum d’appels en simultané.",
     "No other application is using the webcam": "Aucune autre application n’est en train d’utiliser la caméra",
-    "A microphone and webcam are plugged in and set up correctly": "Un microphone et une caméra sont branchées et bien configurés",
+    "A microphone and webcam are plugged in and set up correctly": "Un microphone et une caméra sont branchés et bien configurés",
     "Unable to access webcam / microphone": "Impossible d’accéder à la caméra ou au microphone",
     "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "La fonction a échoué faute de pouvoir accéder au microphone. Vérifiez qu’un microphone est branché et bien configuré.",
     "Unable to access microphone": "Impossible d’accéder au microphone",
@@ -2594,12 +2594,12 @@
     "Angola": "République d’Angola",
     "Andorra": "Andorre",
     "American Samoa": "Samoa américaines",
-    "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invitez quelqu'un via leur nom, e-mail ou nom d'utilisateur (p. ex. <userId/>) ou <a>partagez ce salon</a>.",
-    "Start a conversation with someone using their name, email address or username (like <userId/>).": "Commencer une conversation avec quelqu'un via leur nom, e-mail ou nom d'utilisateur (comme par exemple <userId/>).",
+    "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invitez quelqu’un via son nom, e-mail ou pseudo (p. ex. <userId/>) ou <a>partagez ce salon</a>.",
+    "Start a conversation with someone using their name, email address or username (like <userId/>).": "Commencer une conversation privée avec quelqu’un via son nom, e-mail ou pseudo (comme par exemple <userId/>).",
     "Too Many Calls": "Trop d’appels",
     "Permission is granted to use the webcam": "L’autorisation d’accéder à la caméra a été accordée",
     "Call failed because webcam or microphone could not be accessed. Check that:": "La fonction a échoué faute de pouvoir accéder à la caméra ou au microphone. Vérifiez que :",
-    "Send stickers to this room as you": "Envoyer des stickers dans ce salon en tant que vous-même",
+    "Send stickers to this room as you": "Envoyer des autocollants dans ce salon sous votre nom",
     "Zambia": "Zambie",
     "Yemen": "Yémen",
     "Western Sahara": "Sahara occidental",
@@ -2827,13 +2827,13 @@
     "Bermuda": "Bermudes",
     "with state key %(stateKey)s": "avec la ou les clés d’état %(stateKey)s",
     "with an empty state key": "avec une clé d’état vide",
-    "See when anyone posts a sticker to your active room": "Voir quand n’importe qui envoie un sticker dans le salon actuel",
-    "See when a sticker is posted in this room": "Voir quand un sticker est envoyé dans ce salon",
+    "See when anyone posts a sticker to your active room": "Voir quand n’importe qui envoie un autocollant dans le salon actuel",
+    "See when a sticker is posted in this room": "Voir quand un autocollant est envoyé dans ce salon",
     "See when the avatar changes in your active room": "Voir quand l’avatar change dans le salon actuel",
     "Change the avatar of your active room": "Changer l’avatar du salon actuel",
     "See when the avatar changes in this room": "Voir quand l’avatar change dans ce salon",
     "Change the avatar of this room": "Changer l’avatar de ce salon",
-    "Send stickers into your active room": "Envoyer des stickers dans le salon actuel",
+    "Send stickers into your active room": "Envoyer des autocollants dans le salon actuel",
     "See when the topic changes in this room": "Voir quand le sujet change dans ce salon",
     "See when the topic changes in your active room": "Voir quand le sujet change dans le salon actuel",
     "Change the name of your active room": "Changer le nom du salon actuel",
@@ -2841,10 +2841,10 @@
     "Change the name of this room": "Changer le nom de ce salon",
     "Change the topic of your active room": "Changer le sujet dans le salon actuel",
     "Change the topic of this room": "Changer le sujet de ce salon",
-    "Send stickers into this room": "Envoyer des stickers dans ce salon",
+    "Send stickers into this room": "Envoyer des autocollants dans ce salon",
     "Remain on your screen when viewing another room, when running": "Reste sur votre écran quand vous regardez un autre salon lors de l’appel",
-    "Takes the call in the current room off hold": "Reprend l’appel en cours dans ce salon",
-    "Places the call in the current room on hold": "Met l’appel en pause dans ce salon",
+    "Takes the call in the current room off hold": "Reprend l’appel en attente dans ce salon",
+    "Places the call in the current room on hold": "Met l’appel dans ce salon en attente",
     "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Ajoute (╯°□°)╯︵ ┻━┻ en préfixe du message",
     "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Ajoute ┬──┬ ノ( ゜-゜ノ) en préfixe du message",
     "Effects": "Effets",
@@ -2857,102 +2857,102 @@
     "Send emotes as you in this room": "Envoyer des réactions sous votre nom dans ce salon",
     "See videos posted to your active room": "Voir les vidéos publiées dans votre salon actif",
     "See videos posted to this room": "Voir les vidéos publiées dans ce salon",
-    "Send videos as you in your active room": "Envoie des vidéos en tant que vous-même dans votre salon actuel",
-    "Send videos as you in this room": "Envoie des vidéos en tant que vous dans ce salon",
+    "Send videos as you in your active room": "Envoie des vidéos sous votre nom dans votre salon actuel",
+    "Send videos as you in this room": "Envoie des vidéos sous votre nom dans ce salon",
     "See images posted to this room": "Voir les images publiées dans ce salon",
     "See images posted to your active room": "Voir les images publiées dans votre salon actif",
     "See messages posted to your active room": "Voir les messages publiés dans votre salon actif",
     "See messages posted to this room": "Voir les messages publiés dans ce salon",
-    "Send messages as you in your active room": "Envoie des messages en tant que vous-même dans votre salon actif",
-    "Send messages as you in this room": "Envoie des messages en tant que vous-même dans ce salon",
+    "Send messages as you in your active room": "Envoie des messages sous votre nom dans votre salon actif",
+    "Send messages as you in this room": "Envoie des messages sous votre nom dans ce salon",
     "The <b>%(capability)s</b> capability": "La capacité <b>%(capability)s</b>",
     "See <b>%(eventType)s</b> events posted to your active room": "Voir les événements <b>%(eventType)s</b> publiés dans votre salon actuel",
-    "Send <b>%(eventType)s</b> events as you in your active room": "Envoie des événements <b>%(eventType)s</b> en tant que vous-même dans votre salon actuel",
+    "Send <b>%(eventType)s</b> events as you in your active room": "Envoie des événements <b>%(eventType)s</b> sous votre nom dans votre salon actuel",
     "See <b>%(eventType)s</b> events posted to this room": "Voir les événements <b>%(eventType)s</b> publiés dans ce salon",
-    "Send <b>%(eventType)s</b> events as you in this room": "Envoie des événements <b>%(eventType)s</b> en tant que vous-même dans ce salon",
-    "Send stickers to your active room as you": "Envoie des stickers en tant que vous-même dans le salon actuel",
+    "Send <b>%(eventType)s</b> events as you in this room": "Envoie des événements <b>%(eventType)s</b> sous votre nom dans ce salon",
+    "Send stickers to your active room as you": "Envoie des autocollants sous votre nom dans le salon actuel",
     "Continue with %(ssoButtons)s": "Continuer avec %(ssoButtons)s",
-    "About homeservers": "À propos des serveurs d'accueils",
+    "About homeservers": "À propos des serveurs d’accueil",
     "Learn more": "En savoir plus",
-    "Use your preferred Matrix homeserver if you have one, or host your own.": "Utilisez votre serveur d'accueil Matrix préféré si vous en avez un, ou hébergez le vôtre.",
-    "Other homeserver": "Autre serveur d'accueil",
-    "We call the places where you can host your account ‘homeservers’.": "Nous appelons ‘serveur d'accueils’ les lieux où vous pouvez héberger votre compte.",
-    "Sign into your homeserver": "Connectez-vous sur votre serveur d'accueil",
-    "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org est le plus grand serveur d'accueil dans le monde, donc c'est un bon lieu pour beaucoup.",
-    "Specify a homeserver": "Spécifiez un serveur d'accueil",
+    "Use your preferred Matrix homeserver if you have one, or host your own.": "Utilisez votre serveur d’accueil Matrix préféré si vous en avez un, ou hébergez le vôtre.",
+    "Other homeserver": "Autre serveur d’accueil",
+    "We call the places where you can host your account ‘homeservers’.": "Nous appelons « serveur d’accueil » les lieux où vous pouvez héberger votre compte.",
+    "Sign into your homeserver": "Connectez-vous sur votre serveur d’accueil",
+    "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org est le plus grand serveur d’accueil du monde, ce qui en fait un endroit agréable pour le plus grand nombre.",
+    "Specify a homeserver": "Spécifiez un serveur d’accueil",
     "Invalid URL": "URL invalide",
-    "Unable to validate homeserver": "Impossible de valider le serveur d'accueil",
-    "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>.": "Juste une remarque, si vous n'ajoutez pas un email et que vous oubliez votre mot de passe, vous pourriez <b>perdre définitivement l'accés à votre compte</b>.",
-    "Continuing without email": "Continuer sans email",
+    "Unable to validate homeserver": "Impossible de valider le serveur d’accueil",
+    "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>.": "Juste une remarque, si vous n'ajoutez pas d’e-mail et que vous oubliez votre mot de passe, vous pourriez <b>perdre définitivement l’accès à votre compte</b>.",
+    "Continuing without email": "Continuer sans e-mail",
     "Transfer": "Transférer",
-    "Failed to transfer call": "N'a pas réussi à transférer l'appel",
-    "A call can only be transferred to a single user.": "Un appel peut seulement être transféré à un seul utilisateur.",
-    "Invite by email": "Inviter par email",
-    "Active Widgets": "Gadgets actifs",
+    "Failed to transfer call": "Échec du transfert de l’appel",
+    "A call can only be transferred to a single user.": "Un appel ne peut être transféré qu’à un seul utilisateur.",
+    "Invite by email": "Inviter par e-mail",
+    "Active Widgets": "Widgets actifs",
     "Reason (optional)": "Raison (optionnelle)",
     "Continue with %(provider)s": "Continuer avec %(provider)s",
-    "Homeserver": "Serveur d'accueil",
-    "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.": "Vous pouvez utiliser les options de serveur personnalisés pour vous connecter à d'autres serveurs Matrix en spécifiant une URL de serveur d'accueil différente. Celà vous permet d'utiliser Element avec un compte Matrix existant sur un serveur d'accueil différent.",
+    "Homeserver": "Serveur d’accueil",
+    "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.": "Vous pouvez utiliser les options de serveur personnalisés pour vous connecter à d’autres serveurs Matrix en spécifiant une URL de serveur d’accueil différente. Cela vous permet d’utiliser Element avec un compte Matrix existant sur un serveur d’accueil différent.",
     "Server Options": "Options serveur",
     "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Les messages ici sont chiffrés de bout en bout. Quand les gens joignent, vous pouvez les vérifier dans leur profil, tapez simplement sur leur avatar.",
     "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Les messages ici sont chiffrés de bout en bout. Vérifiez %(displayName)s dans leur profil - tapez sur leur avatar.",
     "Role": "Rôle",
-    "Use the + to make a new room or explore existing ones below": "Utilisez le + pour créer un nouveau salon ou explorer les existantes ci-dessous",
-    "This is the start of <roomName/>.": "C'est le début de <roomName/>.",
+    "Use the + to make a new room or explore existing ones below": "Utilisez le + pour créer un nouveau salon ou explorer ceux existant ci-dessous",
+    "This is the start of <roomName/>.": "C’est le début de <roomName/>.",
     "Add a photo, so people can easily spot your room.": "Ajoutez une photo afin que les gens repèrent facilement votre salon.",
     "%(displayName)s created this room.": "%(displayName)s a créé ce salon.",
     "You created this room.": "Vous avez créé ce salon.",
     "<a>Add a topic</a> to help people know what it is about.": "<a>Ajoutez un sujet</a> pour aider les gens à savoir de quoi il est question.",
-    "Topic: %(topic)s ": "Sujet : %(topic)s ",
-    "Topic: %(topic)s (<a>edit</a>)": "Sujet : %(topic)s (<a>éditer</a>)",
-    "This is the beginning of your direct message history with <displayName/>.": "C'est le début de votre historique de messages direct avec <displayName/>.",
-    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Seulement vous deux êtes dans cette conversation, à moins que l'un de vous invite quelqu'un à joindre.",
+    "Topic: %(topic)s ": "Sujet : %(topic)s ",
+    "Topic: %(topic)s (<a>edit</a>)": "Sujet : %(topic)s (<a>modifier</a>)",
+    "This is the beginning of your direct message history with <displayName/>.": "C’est le début de votre historique de messages privés avec <displayName/>.",
+    "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Vous n’êtes que tous les deux dans cette conversation, à moins que l’un de vous invite quelqu’un.",
     "%(name)s on hold": "%(name)s est en attente",
-    "Return to call": "Revenir à l'appel",
-    "Fill Screen": "Remplir l'écran",
-    "Voice Call": "Appel vocal",
+    "Return to call": "Revenir à l’appel",
+    "Fill Screen": "Remplir l’écran",
+    "Voice Call": "Appel audio",
     "Video Call": "Appel vidéo",
-    "%(peerName)s held the call": "%(peerName)s a mis l'appel en pause",
-    "You held the call <a>Resume</a>": "Vous avez mis l'appel en attente <a>Reprendre</a>",
-    "You held the call <a>Switch</a>": "Vous avez mis l'appel en attente <a>Basculer</a>",
+    "%(peerName)s held the call": "%(peerName)s a mis l’appel en attente",
+    "You held the call <a>Resume</a>": "Vous avez mis l’appel en attente <a>Reprendre</a>",
+    "You held the call <a>Switch</a>": "Vous avez mis l’appel en attente <a>Basculer</a>",
     "sends snowfall": "envoie une chute de neige",
     "Sends the given message with snowfall": "Envoie le message donné avec une chute de neige",
-    "sends fireworks": "envoie des feux d'artifices",
+    "sends fireworks": "envoie des feux d’artifices",
     "Sends the given message with fireworks": "Envoie le message donné avec des feux d'artifices",
     "sends confetti": "envoie des confettis",
     "Sends the given message with confetti": "Envoie le message avec des confettis",
     "Show chat effects": "Montrer les effets cosmétiques du chat",
     "Use Command + Enter to send a message": "Utilisez Ctrl + Entrée pour envoyer un message",
-    "Render LaTeX maths in messages": "Formate et affiche les maths format LaTeX dans les messages",
+    "Render LaTeX maths in messages": "Affiche les formules mathématiques au format LaTeX dans les messages",
     "New version of %(brand)s is available": "Nouvelle version de %(brand)s disponible",
     "Update %(brand)s": "Mettre à jour %(brand)s",
     "Enable desktop notifications": "Activer les notifications sur le bureau",
     "Don't miss a reply": "Ne ratez pas une réponse",
     "See <b>%(msgtype)s</b> messages posted to your active room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans le salon actuel",
     "See <b>%(msgtype)s</b> messages posted to this room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans ce salon",
-    "Send <b>%(msgtype)s</b> messages as you in this room": "Envoie des messages de type<b>%(msgtype)s</b> en tant que vous-même dans ce salon",
-    "Send <b>%(msgtype)s</b> messages as you in your active room": "Envoie des messages de type <b>%(msgtype)s</b> en tant que vous-même dans votre salon actif",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Envoie des messages de type<b>%(msgtype)s</b> sous votre nom dans ce salon",
+    "Send <b>%(msgtype)s</b> messages as you in your active room": "Envoie des messages de type <b>%(msgtype)s</b> sous votre nom dans votre salon actif",
     "See general files posted to your active room": "Voir les fichiers postés dans votre salon actuel",
     "See general files posted to this room": "Voir les fichiers postés dans ce salon",
-    "Send general files as you in your active room": "Envoyer des fichiers en tant que vous-même dans votre salon actif",
-    "Send general files as you in this room": "Envoyer des fichiers en tant que vous-même dans ce salon",
+    "Send general files as you in your active room": "Envoyer des fichiers sous votre nom dans votre salon actif",
+    "Send general files as you in this room": "Envoyer des fichiers sous votre nom dans ce salon",
     "Search (must be enabled)": "Recherche (si activée)",
-    "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Cette session a détecté que votre phrase de passe et clé de sécurité pour les messages sécurisés ont été supprimées.",
-    "A new Security Phrase and key for Secure Messages have been detected.": "Une nouvelle phrase de passe et clé pour les messages sécurisés ont été détectées.",
-    "Make a copy of your Security Key": "Faire une copie de votre Clé de Sécurité",
-    "Confirm your Security Phrase": "Confirmez votre phrase de passe",
-    "Secure your backup with a Security Phrase": "Protégez votre sauvegarde avec une Clé de Sécurité",
-    "Your Security Key is in your <b>Downloads</b> folder.": "Votre Clé de Sécurité est dans le répertoire <b>Téléchargements</b>.",
-    "Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Votre Clé de Sécurité a été <b>copiée dans votre presse-papier</b>, copiez la pour :",
-    "Your Security Key": "Votre Clé de Sécurité",
-    "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Votre Clé de Sécurité est un filet de sécurité. Vous pouvez l’utiliser pour retrouver l’accès à vos messages chiffrés si vous oubliez votre phrase de passe.",
-    "Repeat your Security Phrase...": "Répétez votre phrase de passe…",
-    "Please enter your Security Phrase a second time to confirm.": "Merci de saisir votre phrase de passe une seconde fois pour confirmer.",
-    "Set up with a Security Key": "Configurer avec une Clé de Sécurité",
-    "Great! This Security Phrase looks strong enough.": "Super ! Cette phrase de passe a l’air assez solide.",
-    "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Nous avons stocké une copie chiffrée de vos clés sur notre serveur. Sécurisez vos sauvegardes avec une phrase de passe.",
-    "Use Security Key": "Utiliser la Clé de Sécurité",
-    "Use Security Key or Phrase": "Utilisez votre Clé de Sécurité ou phrase de passe",
+    "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "Cette session a détecté que votre phrase secrète et clé de sécurité pour les messages sécurisés ont été supprimées.",
+    "A new Security Phrase and key for Secure Messages have been detected.": "Une nouvelle phrase secrète et clé de sécurité pour les messages sécurisés ont été détectées.",
+    "Make a copy of your Security Key": "Faire une copie de votre clé de sécurité",
+    "Confirm your Security Phrase": "Confirmez votre phrase secrète",
+    "Secure your backup with a Security Phrase": "Protégez votre sauvegarde avec une clé de sécurité",
+    "Your Security Key is in your <b>Downloads</b> folder.": "Votre clé de sécurité est dans le répertoire <b>Téléchargements</b>.",
+    "Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Votre clé de Sécurité a été <b>copiée dans votre presse-papier</b>, copiez la pour :",
+    "Your Security Key": "Votre clé de sécurité",
+    "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Votre clé de sécurité est un filet de sécurité. Vous pouvez l’utiliser pour retrouver l’accès à vos messages chiffrés si vous oubliez votre phrase secrète.",
+    "Repeat your Security Phrase...": "Répétez votre phrase secrète…",
+    "Please enter your Security Phrase a second time to confirm.": "Merci de saisir votre phrase secrète une seconde fois pour confirmer.",
+    "Set up with a Security Key": "Configurer avec une clé de sécurité",
+    "Great! This Security Phrase looks strong enough.": "Super ! Cette phrase secrète a l’air assez solide.",
+    "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Nous allons stocker une copie chiffrée de vos clés sur notre serveur. Protégez vos sauvegardes avec une phrase secrète.",
+    "Use Security Key": "Utiliser la clé de sécurité",
+    "Use Security Key or Phrase": "Utilisez votre clé de sécurité ou phrase secrète",
     "You have no visible notifications.": "Vous n’avez aucune notification visible.",
     "Upgrade to pro": "Mettre à jour vers pro",
     "Great, that'll help people know it's you": "Super, ceci aidera des personnes à confirmer qu’il s’agit bien de vous",
@@ -2969,19 +2969,19 @@
     "A confirmation email has been sent to %(emailAddress)s": "Un e-mail de confirmation a été envoyé à %(emailAddress)s",
     "Hold": "Mettre en pause",
     "Resume": "Reprendre",
-    "If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Si vous avez oublié votre Clé de Sécurité, vous pouvez <button>définir de nouvelles options de récupération</button>",
-    "Access your secure message history and set up secure messaging by entering your Security Key.": "Accédez à votre historique de messages chiffrés et mettez en place la messagerie sécurisée en entrant votre Clé de Sécurité.",
-    "Not a valid Security Key": "Clé de Sécurité invalide",
-    "This looks like a valid Security Key!": "Ça ressemble à une Clé de Sécurité !",
+    "If you've forgotten your Security Key you can <button>set up new recovery options</button>": "Si vous avez oublié votre clé de sécurité, vous pouvez <button>définir de nouvelles options de récupération</button>",
+    "Access your secure message history and set up secure messaging by entering your Security Key.": "Accédez à votre historique de messages chiffrés et mettez en place la messagerie sécurisée en entrant votre clé de sécurité.",
+    "Not a valid Security Key": "Clé de sécurité invalide",
+    "This looks like a valid Security Key!": "Ça ressemble à une clé de sécurité !",
     "Enter Security Key": "Saisir la clé de sécurité",
-    "If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Si vous avez oublié votre phrase de passe vous pouvez <button1>utiliser votre Clé de Sécurité</button1> ou <button2>définir de nouvelles options de récupération</button2>",
-    "Access your secure message history and set up secure messaging by entering your Security Phrase.": "Accédez à votre historique de messages chiffrés et mettez en place la messagerie sécurisée en entrant votre phrase de passe.",
-    "Enter Security Phrase": "Saisir la phrase de passe",
-    "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "La sauvegarde n’a pas pu être déchiffrée avec cette phrase de passe : merci de vérifier que vous avez saisi la bonne phrase de passe.",
-    "Incorrect Security Phrase": "Phrase de passe incorrecte",
-    "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "La sauvegarde n’a pas pu être déchiffrée avec cette Clé de Sécurité : merci de vérifier que vous avez saisi la bonne Clé de Sécurité.",
-    "Security Key mismatch": "Pas de correspondance entre les Clés de Sécurité",
-    "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Impossible d’accéder à l’espace de stockage sécurisé. Merci de vérifier que vous avez saisi la bonne phrase de passe.",
+    "If you've forgotten your Security Phrase you can <button1>use your Security Key</button1> or <button2>set up new recovery options</button2>": "Si vous avez oublié votre phrase secrète vous pouvez <button1>utiliser votre clé de sécurité</button1> ou <button2>définir de nouvelles options de récupération</button2>",
+    "Access your secure message history and set up secure messaging by entering your Security Phrase.": "Accédez à votre historique de messages chiffrés et mettez en place la messagerie sécurisée en entrant votre phrase secrète.",
+    "Enter Security Phrase": "Saisir la phrase de secrète",
+    "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "La sauvegarde n’a pas pu être déchiffrée avec cette phrase secrète : merci de vérifier que vous avez saisi la bonne phrase secrète.",
+    "Incorrect Security Phrase": "Phrase secrète incorrecte",
+    "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "La sauvegarde n’a pas pu être déchiffrée avec cette clé de sécurité : merci de vérifier que vous avez saisi la bonne clé de sécurité.",
+    "Security Key mismatch": "Pas de correspondance entre les clés de sécurité",
+    "Unable to access secret storage. Please verify that you entered the correct Security Phrase.": "Impossible d’accéder à l’espace de stockage sécurisé. Merci de vérifier que vous avez saisi la bonne phrase secrète.",
     "Invalid Security Key": "Clé de Sécurité invalide",
     "Wrong Security Key": "Mauvaise Clé de Sécurité",
     "Remember this": "Mémoriser ceci",
@@ -2995,14 +2995,14 @@
     "Minimize dialog": "Réduire la modale",
     "Maximize dialog": "Maximiser la modale",
     "%(hostSignupBrand)s Setup": "Configuration de %(hostSignupBrand)s",
-    "You should know": "Vous devriez connaître",
+    "You should know": "Vous devriez prendre connaissance de",
     "Privacy Policy": "Politique de confidentialité",
     "Cookie Policy": "Politique de gestion des cookies",
     "Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.": "Consultez nos <privacyPolicyLink />, <termsOfServiceLink /> et <cookiePolicyLink />.",
-    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Continuer permettra temporairement au processus de configuration de %(hostSignupBrand)s d’accéder à votre compte pour récupérer les adresses email vérifiées. Les données ne sont pas stockées.",
+    "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Continuer permettra temporairement au processus de configuration de %(hostSignupBrand)s d’accéder à votre compte pour récupérer les adresses e-mail vérifiées. Les données ne sont pas stockées.",
     "Failed to connect to your homeserver. Please close this dialog and try again.": "Impossible de vous connecter à votre serveur d’accueil. Merci de fermer cette modale et de réessayer.",
     "Abort": "Annuler",
-    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Êtes-vous sûr de vouloir annuler la création de cet hôte ? Le process ne pourra pas être repris.",
+    "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Êtes-vous sûr de vouloir annuler la création de cet hôte ? Le processus ne pourra pas être repris.",
     "Confirm abort of host creation": "Confirmer l’annulation de la création de cet hôte",
     "There was an error finding this widget.": "Erreur lors de la récupération de ce widget.",
     "Windows": "Windows",
@@ -3010,9 +3010,9 @@
     "Share your screen": "Partager votre écran",
     "Set my room layout for everyone": "Définir ma disposition de salon pour tout le monde",
     "Open dial pad": "Ouvrir le pavé de numérotation",
-    "Start a Conversation": "Démarrer une conversation",
+    "Start a Conversation": "Commencer une conversation",
     "Recently visited rooms": "Salons visités récemment",
-    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Sauvegardez vos clés de chiffrement et les données de votre compte au casoù vous perdiez l’accès à vos sessions. Vos clés seront sécurisés avec une Clé de Sécurité unique.",
+    "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Sauvegardez vos clés de chiffrement et les données de votre compte au cas où vous perdiez l’accès à vos sessions. Vos clés seront sécurisés avec une Clé de Sécurité unique.",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Mettre en cache localement et de manière sécurisée les messages chiffrés pour qu’ils apparaissent dans les résultats de recherche, en utilisant %(size)s pour stocker les messages de %(rooms)s salons.",
     "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Mettre en cache localement et de manière sécurisée les messages chiffrés pour qu’ils apparaissent dans les résultats de recherche, en utilisant %(size)s pour stocker les messages de %(rooms)s salons.",
     "Channel: <channelLink/>": "Canal : <channelLink/>",
@@ -3020,25 +3020,25 @@
     "Dial pad": "Pavé de numérotation",
     "There was an error looking up the phone number": "Erreur lors de la recherche de votre numéro de téléphone",
     "Unable to look up phone number": "Impossible de trouver votre numéro de téléphone",
-    "Use Ctrl + F to search": "Utilisez Control + F pour rechercher",
+    "Use Ctrl + F to search": "Utilisez Ctrl + F pour rechercher",
     "Use Command + F to search": "Utilisez Commande + F pour rechercher",
     "Show line numbers in code blocks": "Afficher les numéros de ligne dans les blocs de code",
-    "Expand code blocks by default": "Dérouler les blocs de code par défaut",
-    "Show stickers button": "Afficher le bouton stickers",
+    "Expand code blocks by default": "Développer les blocs de code par défaut",
+    "Show stickers button": "Afficher le bouton autocollants",
     "Use app": "Utiliser l’application",
     "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web est expérimental sur téléphone. Pour une meilleure expérience et bénéficier des dernières fonctionnalités, utilisez notre application native gratuite.",
     "Use app for a better experience": "Utilisez une application pour une meilleure expérience",
     "See text messages posted to your active room": "Voir les messages textuels dans le salon actif",
     "See text messages posted to this room": "Voir les messages textuels envoyés dans ce salon",
-    "Send text messages as you in your active room": "Envoyez des messages textuels en tant que vous-même dans le salon actif",
-    "Send text messages as you in this room": "Envoyez des messages textuels en tant que vous-même dans ce salon",
+    "Send text messages as you in your active room": "Envoyez des messages textuels sous votre nom dans le salon actif",
+    "Send text messages as you in this room": "Envoyez des messages textuels sous votre nom dans ce salon",
     "See when the name changes in your active room": "Suivre les changements de nom dans le salon actif",
     "Change which room, message, or user you're viewing": "Changer le salon, message, ou la personne que vous visualisez",
     "Change which room you're viewing": "Changer le salon que vous visualisez",
     "Remain on your screen while running": "Reste sur votre écran pendant l’exécution",
     "%(senderName)s has updated the widget layout": "%(senderName)s a mis à jour la disposition du widget",
-    "Converts the DM to a room": "Transforme le message privé en salon",
-    "Converts the room to a DM": "Transforme le salon en message privé",
+    "Converts the DM to a room": "Transforme la conversation privée en salon",
+    "Converts the room to a DM": "Transforme le salon en conversation privée",
     "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Votre serveur d’accueil a rejeté la demande de connexion. Ceci pourrait être dû à une connexion qui prend trop de temps. Si cela persiste, merci de contacter l’administrateur de votre serveur d’accueil.",
     "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Votre serveur d’accueil n’est pas accessible, nous n’avons pas pu vous connecter. Merci de réessayer. Si cela persiste, merci de contacter l’administrateur de votre serveur d’accueil.",
     "Try again": "Réessayez",

From 93f7f13c442c0faefd15d04e512bc3575c784585 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 3 Mar 2021 18:16:27 -0700
Subject: [PATCH 367/389] Early proof of concept for media customization
 support

---
 src/customisations/Media.ts                   | 138 ++++++++++++++++++
 .../models/IMediaEventContent.ts              |  87 +++++++++++
 src/utils/DecryptFile.js                      |   5 +-
 3 files changed, 228 insertions(+), 2 deletions(-)
 create mode 100644 src/customisations/Media.ts
 create mode 100644 src/customisations/models/IMediaEventContent.ts

diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts
new file mode 100644
index 0000000000..27abc6bc50
--- /dev/null
+++ b/src/customisations/Media.ts
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {MatrixClientPeg} from "../MatrixClientPeg";
+import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent";
+
+// Populate this class with the details of your customisations when copying it.
+
+// Implementation note: The Media class must complete the contract as shown here, though
+// the constructor can be whatever is relevant to your implementation. The mediaForX
+// functions below create an instance of the Media class and are used throughout the
+// project.
+
+/**
+ * A media object is a representation of a "source media" and an optional
+ * "thumbnail media", derived from event contents or external sources.
+ */
+export class Media {
+    // Per above, this constructor signature can be whatever is helpful for you.
+    constructor(private prepared: IPreparedMedia) {
+    }
+
+    /**
+     * The MXC URI of the source media.
+     */
+    public get srcMxc(): string {
+        return this.prepared.mxc;
+    }
+
+    /**
+     * The MXC URI of the thumbnail media, if a thumbnail is recorded. Null/undefined
+     * otherwise.
+     */
+    public get thumbnailMxc(): string | undefined | null {
+        return this.prepared.thumbnail?.mxc;
+    }
+
+    /**
+     * Whether or not a thumbnail is recorded for this media.
+     */
+    public get hasThumbnail(): boolean {
+        return !!this.thumbnailMxc;
+    }
+
+    /**
+     * The HTTP URL for the source media.
+     */
+    public get srcHttp(): string {
+        return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc);
+    }
+
+    /**
+     * Gets the HTTP URL for the thumbnail media with the requested characteristics, if a thumbnail
+     * is recorded for this media. Returns null/undefined otherwise.
+     * @param {number} width The desired width of the thumbnail.
+     * @param {number} height The desired height of the thumbnail.
+     * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
+     * @returns {string} The HTTP URL which points to the thumbnail.
+     */
+    public getThumbnailHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string | null | undefined {
+        if (!this.hasThumbnail) return null;
+        return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode);
+    }
+
+    /**
+     * Gets the HTTP URL for a thumbnail of the source media with the requested characteristics.
+     * @param {number} width The desired width of the thumbnail.
+     * @param {number} height The desired height of the thumbnail.
+     * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
+     * @returns {string} The HTTP URL which points to the thumbnail.
+     */
+    public getThumbnailOfSourceHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string {
+        return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode);
+    }
+
+    /**
+     * Downloads the source media.
+     * @returns {Promise<Response>} Resolves to the server's response for chaining.
+     */
+    public downloadSource(): Promise<Response> {
+        return fetch(this.srcHttp);
+    }
+
+    /**
+     * Downloads the thumbnail media with the requested characteristics. If no thumbnail media is present,
+     * this throws an exception.
+     * @param {number} width The desired width of the thumbnail.
+     * @param {number} height The desired height of the thumbnail.
+     * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
+     * @returns {Promise<Response>} Resolves to the server's response for chaining.
+     */
+    public downloadThumbnail(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise<Response> {
+        if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail");
+        return fetch(this.getThumbnailHttp(width, height, mode));
+    }
+
+    /**
+     * Downloads a thumbnail of the source media with the requested characteristics.
+     * @param {number} width The desired width of the thumbnail.
+     * @param {number} height The desired height of the thumbnail.
+     * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
+     * @returns {Promise<Response>} Resolves to the server's response for chaining.
+     */
+    public downloadThumbnailOfSource(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise<Response> {
+        return fetch(this.getThumbnailOfSourceHttp(width, height, mode));
+    }
+}
+
+/**
+ * Creates a media object from event content.
+ * @param {IMediaEventContent} content The event content.
+ * @returns {Media} The media object.
+ */
+export function mediaFromContent(content: IMediaEventContent): Media {
+    return new Media(prepEventContentAsMedia(content));
+}
+
+/**
+ * Creates a media object from an MXC URI.
+ * @param {string} mxc The MXC URI.
+ * @returns {Media} The media object.
+ */
+export function mediaFromMxc(mxc: string): Media {
+    return mediaFromContent({url: mxc});
+}
diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts
new file mode 100644
index 0000000000..0211a63787
--- /dev/null
+++ b/src/customisations/models/IMediaEventContent.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO: These types should be elsewhere.
+
+export interface IEncryptedFile {
+    url: string;
+    key: {
+        alg: string;
+        key_ops: string[];
+        kty: string;
+        k: string;
+        ext: boolean;
+    };
+    iv: string;
+    hashes: {[alg: string]: string};
+    v: string;
+}
+
+export interface IMediaEventContent {
+    url?: string; // required on unencrypted media
+    file?: IEncryptedFile; // required for *encrypted* media
+    info?: {
+        thumbnail_url?: string;
+        thumbnail_file?: IEncryptedFile;
+    };
+}
+
+export interface IPreparedMedia extends IMediaObject {
+    thumbnail?: IMediaObject;
+}
+
+export interface IMediaObject {
+    mxc: string;
+    file?: IEncryptedFile;
+}
+
+/**
+ * Parses an event content body into a prepared media object. This prepared media object
+ * can be used with other functions to manipulate the media.
+ * @param {IMediaEventContent} content Unredacted media event content. See interface.
+ * @returns {IPreparedMedia} A prepared media object.
+ * @throws Throws if the given content cannot be packaged into a prepared media object.
+ */
+export function prepEventContentAsMedia(content: IMediaEventContent): IPreparedMedia {
+    let thumbnail: IMediaObject = null;
+    if (content?.info?.thumbnail_url) {
+        thumbnail = {
+            mxc: content.info.thumbnail_url,
+            file: content.info.thumbnail_file,
+        };
+    } else if (content?.info?.thumbnail_file?.url) {
+        thumbnail = {
+            mxc: content.info.thumbnail_file.url,
+            file: content.info.thumbnail_file,
+        };
+    }
+
+    if (content?.url) {
+        return {
+            thumbnail,
+            mxc: content.url,
+            file: content.file,
+        };
+    } else if (content?.file?.url) {
+        return {
+            thumbnail,
+            mxc: content.file.url,
+            file: content.file,
+        };
+    }
+
+    throw new Error("Invalid file provided: cannot determine MXC URI. Has it been redacted?");
+}
diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js
index d3625d614a..fb3600cd79 100644
--- a/src/utils/DecryptFile.js
+++ b/src/utils/DecryptFile.js
@@ -19,6 +19,7 @@ limitations under the License.
 import encrypt from 'browser-encrypt-attachment';
 // Grab the client so that we can turn mxc:// URLs into https:// URLS.
 import {MatrixClientPeg} from '../MatrixClientPeg';
+import {mediaFromContent} from "../customisations/Media";
 
 // WARNING: We have to be very careful about what mime-types we allow into blobs,
 // as for performance reasons these are now rendered via URL.createObjectURL()
@@ -87,9 +88,9 @@ const ALLOWED_BLOB_MIMETYPES = {
  * @returns {Promise}
  */
 export function decryptFile(file) {
-    const url = MatrixClientPeg.get().mxcUrlToHttp(file.url);
+    const media = mediaFromContent({file});
     // Download the encrypted file as an array buffer.
-    return Promise.resolve(fetch(url)).then(function(response) {
+    return media.downloadSource().then(function(response) {
         return response.arrayBuffer();
     }).then(function(responseData) {
         // Decrypt the array buffer using the information taken from

From 53935782bcaa51daab0011ff16236943e994f153 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 3 Mar 2021 18:22:57 -0700
Subject: [PATCH 368/389] Convert DecryptFile to TS and modernize a bit

---
 .../models/IMediaEventContent.ts              |  1 +
 src/utils/{DecryptFile.js => DecryptFile.ts}  | 63 +++++++++----------
 2 files changed, 30 insertions(+), 34 deletions(-)
 rename src/utils/{DecryptFile.js => DecryptFile.ts} (76%)

diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts
index 0211a63787..4cbe07dbd5 100644
--- a/src/customisations/models/IMediaEventContent.ts
+++ b/src/customisations/models/IMediaEventContent.ts
@@ -18,6 +18,7 @@
 
 export interface IEncryptedFile {
     url: string;
+    mimetype?: string;
     key: {
         alg: string;
         key_ops: string[];
diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.ts
similarity index 76%
rename from src/utils/DecryptFile.js
rename to src/utils/DecryptFile.ts
index fb3600cd79..93cedbc707 100644
--- a/src/utils/DecryptFile.js
+++ b/src/utils/DecryptFile.ts
@@ -1,6 +1,5 @@
 /*
-Copyright 2016 OpenMarket Ltd
-Copyright 2018 New Vector Ltd
+Copyright 2016, 2018, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -17,9 +16,8 @@ limitations under the License.
 
 // Pull in the encryption lib so that we can decrypt attachments.
 import encrypt from 'browser-encrypt-attachment';
-// Grab the client so that we can turn mxc:// URLs into https:// URLS.
-import {MatrixClientPeg} from '../MatrixClientPeg';
 import {mediaFromContent} from "../customisations/Media";
+import {IEncryptedFile} from "../customisations/models/IMediaEventContent";
 
 // WARNING: We have to be very careful about what mime-types we allow into blobs,
 // as for performance reasons these are now rendered via URL.createObjectURL()
@@ -55,48 +53,46 @@ import {mediaFromContent} from "../customisations/Media";
 // For the record, mime-types which must NEVER enter this list below include:
 //   text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
 
-const ALLOWED_BLOB_MIMETYPES = {
-    'image/jpeg': true,
-    'image/gif': true,
-    'image/png': true,
+const ALLOWED_BLOB_MIMETYPES = [
+    'image/jpeg',
+    'image/gif',
+    'image/png',
 
-    'video/mp4': true,
-    'video/webm': true,
-    'video/ogg': true,
+    'video/mp4',
+    'video/webm',
+    'video/ogg',
 
-    'audio/mp4': true,
-    'audio/webm': true,
-    'audio/aac': true,
-    'audio/mpeg': true,
-    'audio/ogg': true,
-    'audio/wave': true,
-    'audio/wav': true,
-    'audio/x-wav': true,
-    'audio/x-pn-wav': true,
-    'audio/flac': true,
-    'audio/x-flac': true,
-};
+    'audio/mp4',
+    'audio/webm',
+    'audio/aac',
+    'audio/mpeg',
+    'audio/ogg',
+    'audio/wave',
+    'audio/wav',
+    'audio/x-wav',
+    'audio/x-pn-wav',
+    'audio/flac',
+    'audio/x-flac',
+];
 
 /**
  * Decrypt a file attached to a matrix event.
- * @param {Object} file The json taken from the matrix event.
+ * @param {IEncryptedFile} file The json taken from the matrix event.
  *   This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments}
  *   as the encryption info object, so will also have the those keys in addition to
  *   the keys below.
- * @param {string} file.url An mxc:// URL for the encrypted file.
- * @param {string} file.mimetype The MIME-type of the plaintext file.
- * @returns {Promise}
+ * @returns {Promise<Blob>} Resolves to a Blob of the file.
  */
-export function decryptFile(file) {
+export function decryptFile(file: IEncryptedFile): Promise<Blob> {
     const media = mediaFromContent({file});
     // Download the encrypted file as an array buffer.
-    return media.downloadSource().then(function(response) {
+    return media.downloadSource().then((response) => {
         return response.arrayBuffer();
-    }).then(function(responseData) {
+    }).then((responseData) => {
         // Decrypt the array buffer using the information taken from
         // the event content.
         return encrypt.decryptAttachment(responseData, file);
-    }).then(function(dataArray) {
+    }).then((dataArray) => {
         // Turn the array into a Blob and give it the correct MIME-type.
 
         // IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise
@@ -104,11 +100,10 @@ export function decryptFile(file) {
         // browser (e.g. by copying the URI into a new tab or window.)
         // See warning at top of file.
         let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
-        if (!ALLOWED_BLOB_MIMETYPES[mimetype]) {
+        if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
             mimetype = 'application/octet-stream';
         }
 
-        const blob = new Blob([dataArray], {type: mimetype});
-        return blob;
+        return new Blob([dataArray], {type: mimetype});
     });
 }

From 1ac12479ca7b132b9d9d4eddc64ed6aa9b7fbde0 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 3 Mar 2021 19:06:46 -0700
Subject: [PATCH 369/389] Convert cases of mxcUrlToHttp to new media
 customisation

---
 src/HtmlUtils.tsx                             |  9 ++---
 src/Notifier.ts                               |  3 +-
 src/autocomplete/CommunityProvider.tsx        |  3 +-
 src/components/structures/GroupView.js        | 11 +++---
 src/components/structures/LeftPanel.tsx       |  3 +-
 .../structures/SpaceRoomDirectory.tsx         | 15 ++------
 src/components/views/avatars/GroupAvatar.tsx  |  9 +++--
 .../views/dialogs/ConfirmUserActionDialog.js  |  6 ++-
 .../dialogs/EditCommunityPrototypeDialog.tsx  |  3 +-
 .../views/dialogs/IncomingSasDialog.js        | 20 +++++-----
 src/components/views/elements/AddressTile.js  |  5 +--
 src/components/views/elements/Flair.js        |  4 +-
 src/components/views/elements/Pill.js         |  3 +-
 src/components/views/elements/SSOButtons.tsx  |  3 +-
 src/components/views/elements/TagTile.js      | 13 ++++---
 .../views/groups/GroupInviteTile.js           |  6 ++-
 .../views/groups/GroupMemberTile.js           |  8 ++--
 src/components/views/groups/GroupRoomInfo.js  |  7 ++--
 src/components/views/groups/GroupRoomTile.js  |  8 ++--
 src/components/views/groups/GroupTile.js      |  6 ++-
 src/components/views/messages/MAudioBody.js   |  7 ++--
 src/components/views/messages/MFileBody.js    |  5 ++-
 src/components/views/messages/MImageBody.js   | 36 +++++++-----------
 src/components/views/messages/MVideoBody.tsx  | 14 ++++---
 .../views/messages/RoomAvatarEvent.js         |  3 +-
 src/components/views/right_panel/UserInfo.tsx |  3 +-
 .../room_settings/RoomProfileSettings.js      |  5 ++-
 .../views/rooms/LinkPreviewWidget.js          |  9 +++--
 src/components/views/settings/ChangeAvatar.js |  3 +-
 .../views/settings/ProfileSettings.js         |  5 ++-
 src/customisations/Media.ts                   | 38 +++++++++++++++++--
 src/customisations/models/ResizeMode.ts       | 17 +++++++++
 src/stores/OwnProfileStore.ts                 |  9 ++++-
 33 files changed, 178 insertions(+), 121 deletions(-)
 create mode 100644 src/customisations/models/ResizeMode.ts

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 7d6b049914..12752eb20f 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -36,6 +36,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
 import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
 import ReplyThread from "./components/views/elements/ReplyThread";
+import {mediaFromMxc} from "./customisations/Media";
 
 linkifyMatrix(linkify);
 
@@ -181,11 +182,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
         if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
             return { tagName, attribs: {}};
         }
-        attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
-            attribs.src,
-            attribs.width || 800,
-            attribs.height || 600,
-        );
+        const width = Number(attribs.width) || 800;
+        const height = Number(attribs.height) || 600;
+        attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height);
         return { tagName, attribs };
     },
     'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
diff --git a/src/Notifier.ts b/src/Notifier.ts
index 6460be20ad..f68bfabc18 100644
--- a/src/Notifier.ts
+++ b/src/Notifier.ts
@@ -36,6 +36,7 @@ import {SettingLevel} from "./settings/SettingLevel";
 import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
 import RoomViewStore from "./stores/RoomViewStore";
 import UserActivity from "./UserActivity";
+import {mediaFromMxc} from "./customisations/Media";
 
 /*
  * Dispatches:
@@ -150,7 +151,7 @@ export const Notifier = {
         // Ideally in here we could use MSC1310 to detect the type of file, and reject it.
 
         return {
-            url: MatrixClientPeg.get().mxcUrlToHttp(content.url),
+            url: mediaFromMxc(content.url).srcHttp,
             name: content.name,
             type: content.type,
             size: content.size,
diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx
index ebf5d536ec..b7a4e0960e 100644
--- a/src/autocomplete/CommunityProvider.tsx
+++ b/src/autocomplete/CommunityProvider.tsx
@@ -27,6 +27,7 @@ import {sortBy} from "lodash";
 import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
 import {ICompletion, ISelectionRange} from "./Autocompleter";
 import FlairStore from "../stores/FlairStore";
+import {mediaFromMxc} from "../customisations/Media";
 
 const COMMUNITY_REGEX = /\B\+\S*/g;
 
@@ -95,7 +96,7 @@ export default class CommunityProvider extends AutocompleteProvider {
                             name={name || groupId}
                             width={24}
                             height={24}
-                            url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
+                            url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} />
                     </PillCompletion>
                 ),
                 range,
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index b4b871a0b4..f05d8d0758 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -39,6 +39,7 @@ import {Group} from "matrix-js-sdk";
 import {allSettled, sleep} from "../../utils/promise";
 import RightPanelStore from "../../stores/RightPanelStore";
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import {mediaFromMxc} from "../../customisations/Media";
 import {replaceableComponent} from "../../utils/replaceableComponent";
 
 const LONG_DESC_PLACEHOLDER = _td(
@@ -368,8 +369,7 @@ class FeaturedUser extends React.Component {
 
         const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
         const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
-        const httpUrl = MatrixClientPeg.get()
-            .mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
+        const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64);
 
         const deleteButton = this.props.editing ?
             <img
@@ -981,10 +981,9 @@ export default class GroupView extends React.Component {
                     <Spinner />
                 </div>;
             }
-            const httpInviterAvatar = this.state.inviterProfile ?
-                this._matrixClient.mxcUrlToHttp(
-                    this.state.inviterProfile.avatarUrl, 36, 36,
-                ) : null;
+            const httpInviterAvatar = this.state.inviterProfile
+                ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
+                : null;
 
             const inviter = group.inviter || {};
             let inviterName = inviter.userId;
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index 88c7a71b35..f7865d094a 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -41,6 +41,7 @@ import RoomListNumResults from "../views/rooms/RoomListNumResults";
 import LeftPanelWidget from "./LeftPanelWidget";
 import SpacePanel from "../views/spaces/SpacePanel";
 import {replaceableComponent} from "../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../customisations/Media";
 
 interface IProps {
     isMinimized: boolean;
@@ -121,7 +122,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
         let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
         const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
         if (settingBgMxc) {
-            avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize);
+            avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
         }
 
         const avatarUrlProp = `url(${avatarUrl})`;
diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index 72e52678b6..9ee16558d3 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -34,6 +34,7 @@ import {EnhancedMap} from "../../utils/maps";
 import StyledCheckbox from "../views/elements/StyledCheckbox";
 import AutoHideScrollbar from "./AutoHideScrollbar";
 import BaseAvatar from "../views/avatars/BaseAvatar";
+import {mediaFromMxc} from "../../customisations/Media";
 
 interface IProps {
     space: Room;
@@ -158,12 +159,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
 
     let url: string;
     if (space.avatar_url) {
-        url = MatrixClientPeg.get().mxcUrlToHttp(
-            space.avatar_url,
-            Math.floor(24 * window.devicePixelRatio),
-            Math.floor(24 * window.devicePixelRatio),
-            "crop",
-        );
+        url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio));
     }
 
     return <div className="mx_SpaceRoomDirectory_subspace">
@@ -265,12 +261,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
 
     let url: string;
     if (room.avatar_url) {
-        url = cli.mxcUrlToHttp(
-            room.avatar_url,
-            Math.floor(32 * window.devicePixelRatio),
-            Math.floor(32 * window.devicePixelRatio),
-            "crop",
-        );
+        url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio));
     }
 
     const content = <React.Fragment>
diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx
index a033257871..dc363da304 100644
--- a/src/components/views/avatars/GroupAvatar.tsx
+++ b/src/components/views/avatars/GroupAvatar.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2017 Vector Creations Ltd
+Copyright 2017, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,6 +18,8 @@ import React from 'react';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import BaseAvatar from './BaseAvatar';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
+import {ResizeMode} from "../../../customisations/models/ResizeMode";
 
 export interface IProps {
         groupId?: string;
@@ -25,7 +27,7 @@ export interface IProps {
         groupAvatarUrl?: string;
         width?: number;
         height?: number;
-        resizeMethod?: string;
+        resizeMethod?: ResizeMode;
         onClick?: React.MouseEventHandler;
 }
 
@@ -38,8 +40,7 @@ export default class GroupAvatar extends React.Component<IProps> {
     };
 
     getGroupAvatarUrl() {
-        return MatrixClientPeg.get().mxcUrlToHttp(
-            this.props.groupAvatarUrl,
+        return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp(
             this.props.width,
             this.props.height,
             this.props.resizeMethod,
diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js
index 8827f161f1..8cfd28986b 100644
--- a/src/components/views/dialogs/ConfirmUserActionDialog.js
+++ b/src/components/views/dialogs/ConfirmUserActionDialog.js
@@ -21,6 +21,7 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import { GroupMemberType } from '../../../groups';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 /*
  * A dialog for confirming an operation on another user.
@@ -108,8 +109,9 @@ export default class ConfirmUserActionDialog extends React.Component {
             name = this.props.member.name;
             userId = this.props.member.userId;
         } else {
-            const httpAvatarUrl = this.props.groupMember.avatarUrl ?
-                this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
+            const httpAvatarUrl = this.props.groupMember.avatarUrl
+                ? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48)
+                : null;
             name = this.props.groupMember.displayname || this.props.groupMember.userId;
             userId = this.props.groupMember.userId;
             avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
index 504d563bd9..ee3696b427 100644
--- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
+++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
@@ -24,6 +24,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
 import FlairStore from "../../../stores/FlairStore";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 interface IProps extends IDialogProps {
     communityId: string;
@@ -118,7 +119,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
         let preview = <img src={this.state.avatarPreview} className="mx_EditCommunityPrototypeDialog_avatar" />;
         if (!this.state.avatarPreview) {
             if (this.state.currentAvatarUrl) {
-                const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl);
+                const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
                 preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
             } else {
                 preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />
diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js
index d65ec7563f..f18b7a9d0c 100644
--- a/src/components/views/dialogs/IncomingSasDialog.js
+++ b/src/components/views/dialogs/IncomingSasDialog.js
@@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 const PHASE_START = 0;
 const PHASE_SHOW_SAS = 1;
@@ -123,22 +124,21 @@ export default class IncomingSasDialog extends React.Component {
         const Spinner = sdk.getComponent("views.elements.Spinner");
         const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
 
-        const isSelf = this.props.verifier.userId == MatrixClientPeg.get().getUserId();
+        const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId();
 
         let profile;
-        if (this.state.opponentProfile) {
+        const oppProfile = this.state.opponentProfile;
+        if (oppProfile) {
+            const url = oppProfile.avatar_url
+                ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio))
+                : null;
             profile = <div className="mx_IncomingSasDialog_opponentProfile">
-                <BaseAvatar name={this.state.opponentProfile.displayname}
+                <BaseAvatar name={oppProfile.displayname}
                     idName={this.props.verifier.userId}
-                    url={MatrixClientPeg.get().mxcUrlToHttp(
-                        this.state.opponentProfile.avatar_url,
-                        Math.floor(48 * window.devicePixelRatio),
-                        Math.floor(48 * window.devicePixelRatio),
-                        'crop',
-                    )}
+                    url={url}
                     width={48} height={48} resizeMethod='crop'
                 />
-                <h2>{this.state.opponentProfile.displayname}</h2>
+                <h2>{oppProfile.displayname}</h2>
             </div>;
         } else if (this.state.opponentProfileError) {
             profile = <div>
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js
index 4a216dbae4..4f5ee45a3c 100644
--- a/src/components/views/elements/AddressTile.js
+++ b/src/components/views/elements/AddressTile.js
@@ -23,6 +23,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import { _t } from '../../../languageHandler';
 import { UserAddressType } from '../../../UserAddress.js';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 @replaceableComponent("views.elements.AddressTile")
 export default class AddressTile extends React.Component {
@@ -47,9 +48,7 @@ export default class AddressTile extends React.Component {
         const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType);
 
         if (isMatrixAddress && address.avatarMxc) {
-            imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp(
-                address.avatarMxc, 25, 25, 'crop',
-            ));
+            imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25));
         } else if (address.addressType === 'email') {
             imgUrls.push(require("../../../../res/img/icon-email-user.svg"));
         }
diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js
index 75998cb721..73d5b91511 100644
--- a/src/components/views/elements/Flair.js
+++ b/src/components/views/elements/Flair.js
@@ -20,6 +20,7 @@ import FlairStore from '../../../stores/FlairStore';
 import dis from '../../../dispatcher/dispatcher';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 
 class FlairAvatar extends React.Component {
@@ -39,8 +40,7 @@ class FlairAvatar extends React.Component {
     }
 
     render() {
-        const httpUrl = this.context.mxcUrlToHttp(
-            this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
+        const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16);
         const tooltip = this.props.groupProfile.name ?
             `${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
             this.props.groupProfile.groupId;
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index b0d4fc7fa2..bf99ee6078 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -26,6 +26,7 @@ import FlairStore from "../../../stores/FlairStore";
 import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {Action} from "../../../dispatcher/actions";
+import {mediaFromMxc} from "../../../customisations/Media";
 import Tooltip from './Tooltip';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 
@@ -259,7 +260,7 @@ class Pill extends React.Component {
                     linkText = groupId;
                     if (this.props.shouldShowPillAvatar) {
                         avatar = <BaseAvatar name={name || groupId} width={16} height={16} aria-hidden="true"
-                                             url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
+                                             url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
                     }
                     pillClass = 'mx_GroupPill';
                 }
diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx
index 3a03252ebd..4e41db0ae7 100644
--- a/src/components/views/elements/SSOButtons.tsx
+++ b/src/components/views/elements/SSOButtons.tsx
@@ -24,6 +24,7 @@ import AccessibleButton from "./AccessibleButton";
 import {_t} from "../../../languageHandler";
 import {IdentityProviderBrand, IIdentityProvider, ISSOFlow} from "../../../Login";
 import AccessibleTooltipButton from "./AccessibleTooltipButton";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 interface ISSOButtonProps extends Omit<IProps, "flow"> {
     idp: IIdentityProvider;
@@ -72,7 +73,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
         brandClass = `mx_SSOButton_brand_${brandName}`;
         icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
     } else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
-        const src = matrixClient.mxcUrlToHttp(idp.icon, 24, 24, "crop", true);
+        const src = mediaFromMxc(idp.icon).getSquareThumbnailHttp(24);
         icon = <img src={src} height="24" width="24" alt={idp.name} />;
     }
 
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index 663acd6329..03d853babc 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -30,6 +30,7 @@ import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import AccessibleButton from "./AccessibleButton";
 import SettingsStore from "../../../settings/SettingsStore";
+import {mediaFromMxc} from "../../../customisations/Media";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 // A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
@@ -130,11 +131,11 @@ export default class TagTile extends React.Component {
         const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
         const profile = this.state.profile || {};
         const name = profile.name || this.props.tag;
-        const avatarHeight = 32;
+        const avatarSize = 32;
 
-        const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
-            profile.avatarUrl, avatarHeight, avatarHeight, "crop",
-        ) : null;
+        const httpUrl = profile.avatarUrl
+            ? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarSize)
+            : null;
 
         const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
         const className = classNames({
@@ -180,8 +181,8 @@ export default class TagTile extends React.Component {
                     name={name}
                     idName={this.props.tag}
                     url={httpUrl}
-                    width={avatarHeight}
-                    height={avatarHeight}
+                    width={avatarSize}
+                    height={avatarSize}
                 />
                 {contextButton}
                 {badgeElement}
diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js
index dc48c01acb..bc0bf966f9 100644
--- a/src/components/views/groups/GroupInviteTile.js
+++ b/src/components/views/groups/GroupInviteTile.js
@@ -27,6 +27,7 @@ import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/Contex
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 // XXX this class copies a lot from RoomTile.js
 @replaceableComponent("views.groups.GroupInviteTile")
@@ -117,8 +118,9 @@ export default class GroupInviteTile extends React.Component {
         const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
 
         const groupName = this.props.group.name || this.props.group.groupId;
-        const httpAvatarUrl = this.props.group.avatarUrl ?
-            this.context.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null;
+        const httpAvatarUrl = this.props.group.avatarUrl
+            ? mediaFromMxc(this.props.group.avatarUrl).getSquareThumbnailHttp(24)
+            : null;
 
         const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
 
diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js
index e8285803b0..a436f2403e 100644
--- a/src/components/views/groups/GroupMemberTile.js
+++ b/src/components/views/groups/GroupMemberTile.js
@@ -23,6 +23,7 @@ import dis from '../../../dispatcher/dispatcher';
 import { GroupMemberType } from '../../../groups';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 @replaceableComponent("views.groups.GroupMemberTile")
 export default class GroupMemberTile extends React.Component {
@@ -46,10 +47,9 @@ export default class GroupMemberTile extends React.Component {
         const EntityTile = sdk.getComponent('rooms.EntityTile');
 
         const name = this.props.member.displayname || this.props.member.userId;
-        const avatarUrl = this.context.mxcUrlToHttp(
-            this.props.member.avatarUrl,
-            36, 36, 'crop',
-        );
+        const avatarUrl = this.props.member.avatarUrl
+            ? mediaFromMxc(this.props.member.avatarUrl).getSquareThumbnailHttp(36)
+            : null;
 
         const av = (
             <BaseAvatar
diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js
index 227a17e995..c1d1adb0b3 100644
--- a/src/components/views/groups/GroupRoomInfo.js
+++ b/src/components/views/groups/GroupRoomInfo.js
@@ -25,6 +25,7 @@ import GroupStore from '../../../stores/GroupStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 @replaceableComponent("views.groups.GroupRoomInfo")
 export default class GroupRoomInfo extends React.Component {
@@ -204,10 +205,8 @@ export default class GroupRoomInfo extends React.Component {
         const avatarUrl = this.state.groupRoom.avatarUrl;
         let avatarElement;
         if (avatarUrl) {
-            const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
-            avatarElement = (<div className="mx_MemberInfo_avatar">
-                            <img src={httpUrl} />
-                        </div>);
+            const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800);
+            avatarElement = <div className="mx_MemberInfo_avatar"><img src={httpUrl} /></div>;
         }
 
         const groupRoomName = this.state.groupRoom.displayname;
diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js
index 8b25437f71..7edfc1a376 100644
--- a/src/components/views/groups/GroupRoomTile.js
+++ b/src/components/views/groups/GroupRoomTile.js
@@ -21,6 +21,7 @@ import dis from '../../../dispatcher/dispatcher';
 import { GroupRoomType } from '../../../groups';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 @replaceableComponent("views.groups.GroupRoomTile")
 class GroupRoomTile extends React.Component {
@@ -42,10 +43,9 @@ class GroupRoomTile extends React.Component {
     render() {
         const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
         const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
-        const avatarUrl = this.context.mxcUrlToHttp(
-            this.props.groupRoom.avatarUrl,
-            36, 36, 'crop',
-        );
+        const avatarUrl = this.props.groupRoom.avatarUrl
+            ? mediaFromMxc(this.props.groupRoom.avatarUrl).getSquareThumbnailHttp(36)
+            : null;
 
         const av = (
             <BaseAvatar name={this.props.groupRoom.displayname}
diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js
index bb1714c9f2..42a977fb79 100644
--- a/src/components/views/groups/GroupTile.js
+++ b/src/components/views/groups/GroupTile.js
@@ -22,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher';
 import FlairStore from '../../../stores/FlairStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 function nop() {}
 
@@ -73,8 +74,9 @@ class GroupTile extends React.Component {
         const descElement = this.props.showDescription ?
             <div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
             <div />;
-        const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp(
-            profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null;
+        const httpUrl = profile.avatarUrl
+            ? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
+            : null;
 
         let avatarElement = (
             <div className="mx_GroupTile_avatar">
diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js
index 498e2db12a..78ded9a514 100644
--- a/src/components/views/messages/MAudioBody.js
+++ b/src/components/views/messages/MAudioBody.js
@@ -22,6 +22,7 @@ import { decryptFile } from '../../../utils/DecryptFile';
 import { _t } from '../../../languageHandler';
 import InlineSpinner from '../elements/InlineSpinner';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromContent} from "../../../customisations/Media";
 
 @replaceableComponent("views.messages.MAudioBody")
 export default class MAudioBody extends React.Component {
@@ -41,11 +42,11 @@ export default class MAudioBody extends React.Component {
     }
 
     _getContentUrl() {
-        const content = this.props.mxEvent.getContent();
-        if (content.file !== undefined) {
+        const media = mediaFromContent(this.props.mxEvent.getContent());
+        if (media.isEncrypted) {
             return this.state.decryptedUrl;
         } else {
-            return MatrixClientPeg.get().mxcUrlToHttp(content.url);
+            return media.srcHttp;
         }
     }
 
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index e9893f99b6..07d7beb793 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -27,6 +27,7 @@ import request from 'browser-request';
 import Modal from '../../../Modal';
 import AccessibleButton from "../elements/AccessibleButton";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromContent} from "../../../customisations/Media";
 
 
 // A cached tinted copy of require("../../../../res/img/download.svg")
@@ -178,8 +179,8 @@ export default class MFileBody extends React.Component {
     }
 
     _getContentUrl() {
-        const content = this.props.mxEvent.getContent();
-        return MatrixClientPeg.get().mxcUrlToHttp(content.url);
+        const media = mediaFromContent(this.props.mxEvent.getContent());
+        return media.srcHttp;
     }
 
     componentDidMount() {
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 59c5b4e66b..0a1f875935 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -28,6 +28,7 @@ import SettingsStore from "../../../settings/SettingsStore";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import InlineSpinner from '../elements/InlineSpinner';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromContent} from "../../../customisations/Media";
 
 @replaceableComponent("views.messages.MImageBody")
 export default class MImageBody extends React.Component {
@@ -167,16 +168,16 @@ export default class MImageBody extends React.Component {
     }
 
     _getContentUrl() {
-        const content = this.props.mxEvent.getContent();
-        if (content.file !== undefined) {
+        const media = mediaFromContent(this.props.mxEvent.getContent());
+        if (media.isEncrypted) {
             return this.state.decryptedUrl;
         } else {
-            return this.context.mxcUrlToHttp(content.url);
+            return media.srcHttp;
         }
     }
 
     _getThumbUrl() {
-        // FIXME: the dharma skin lets images grow as wide as you like, rather than capped to 800x600.
+        // FIXME: we let images grow as wide as you like, rather than capped to 800x600.
         // So either we need to support custom timeline widths here, or reimpose the cap, otherwise the
         // thumbnail resolution will be unnecessarily reduced.
         // custom timeline widths seems preferable.
@@ -185,21 +186,19 @@ export default class MImageBody extends React.Component {
         const thumbHeight = Math.round(600 * pixelRatio);
 
         const content = this.props.mxEvent.getContent();
-        if (content.file !== undefined) {
+        const media = mediaFromContent(content);
+
+        if (media.isEncrypted) {
             // Don't use the thumbnail for clients wishing to autoplay gifs.
             if (this.state.decryptedThumbnailUrl) {
                 return this.state.decryptedThumbnailUrl;
             }
             return this.state.decryptedUrl;
-        } else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) {
+        } else if (content.info && content.info.mimetype === "image/svg+xml" && media.hasThumbnail) {
             // special case to return clientside sender-generated thumbnails for SVGs, if any,
             // given we deliberately don't thumbnail them serverside to prevent
             // billion lol attacks and similar
-            return this.context.mxcUrlToHttp(
-                content.info.thumbnail_url,
-                thumbWidth,
-                thumbHeight,
-            );
+            return media.getThumbnailHttp(thumbWidth, thumbHeight, 'scale');
         } else {
             // we try to download the correct resolution
             // for hi-res images (like retina screenshots).
@@ -218,7 +217,7 @@ export default class MImageBody extends React.Component {
                 pixelRatio === 1.0 ||
                 (!info || !info.w || !info.h || !info.size)
             ) {
-                return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight);
+                return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
             } else {
                 // we should only request thumbnails if the image is bigger than 800x600
                 // (or 1600x1200 on retina) otherwise the image in the timeline will just
@@ -233,24 +232,17 @@ export default class MImageBody extends React.Component {
                     info.w > thumbWidth ||
                     info.h > thumbHeight
                 );
-                const isLargeFileSize = info.size > 1*1024*1024;
+                const isLargeFileSize = info.size > 1*1024*1024; // 1mb
 
                 if (isLargeFileSize && isLargerThanThumbnail) {
                     // image is too large physically and bytewise to clutter our timeline so
                     // we ask for a thumbnail, despite knowing that it will be max 800x600
                     // despite us being retina (as synapse doesn't do 1600x1200 thumbs yet).
-                    return this.context.mxcUrlToHttp(
-                        content.url,
-                        thumbWidth,
-                        thumbHeight,
-                    );
+                    return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight);
                 } else {
                     // download the original image otherwise, so we can scale it client side
                     // to take pixelRatio into account.
-                    // ( no width/height means we want the original image)
-                    return this.context.mxcUrlToHttp(
-                        content.url,
-                    );
+                    return media.srcHttp;
                 }
             }
         }
diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx
index 89985dee7d..32b071ea24 100644
--- a/src/components/views/messages/MVideoBody.tsx
+++ b/src/components/views/messages/MVideoBody.tsx
@@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import InlineSpinner from '../elements/InlineSpinner';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromContent} from "../../../customisations/Media";
 
 interface IProps {
     /* the MatrixEvent to show */
@@ -76,11 +77,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
     }
 
     private getContentUrl(): string|null {
-        const content = this.props.mxEvent.getContent();
-        if (content.file !== undefined) {
+        const media = mediaFromContent(this.props.mxEvent.getContent());
+        if (media.isEncrypted) {
             return this.state.decryptedUrl;
         } else {
-            return MatrixClientPeg.get().mxcUrlToHttp(content.url);
+            return media.srcHttp;
         }
     }
 
@@ -91,10 +92,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
 
     private getThumbUrl(): string|null {
         const content = this.props.mxEvent.getContent();
-        if (content.file !== undefined) {
+        const media = mediaFromContent(content);
+        if (media.isEncrypted) {
             return this.state.decryptedThumbnailUrl;
-        } else if (content.info && content.info.thumbnail_url) {
-            return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
+        } else if (media.hasThumbnail) {
+            return media.thumbnailHttp;
         } else {
             return null;
         }
diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js
index ba860216f0..00aaf9bfda 100644
--- a/src/components/views/messages/RoomAvatarEvent.js
+++ b/src/components/views/messages/RoomAvatarEvent.js
@@ -24,6 +24,7 @@ import * as sdk from '../../../index';
 import Modal from '../../../Modal';
 import AccessibleButton from '../elements/AccessibleButton';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 @replaceableComponent("views.messages.RoomAvatarEvent")
 export default class RoomAvatarEvent extends React.Component {
@@ -35,7 +36,7 @@ export default class RoomAvatarEvent extends React.Component {
     onAvatarClick = () => {
         const cli = MatrixClientPeg.get();
         const ev = this.props.mxEvent;
-        const httpUrl = cli.mxcUrlToHttp(ev.getContent().url);
+        const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp;
 
         const room = cli.getRoom(this.props.mxEvent.getRoomId());
         const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', {
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index eb47a56269..d415d19852 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -63,6 +63,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
 import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
 import RoomAvatar from "../avatars/RoomAvatar";
 import RoomName from "../elements/RoomName";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 interface IDevice {
     deviceId: string;
@@ -1408,7 +1409,7 @@ const UserInfoHeader: React.FC<{
         const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
         if (!avatarUrl) return;
 
-        const httpUrl = cli.mxcUrlToHttp(avatarUrl);
+        const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
         const params = {
             src: httpUrl,
             name: member.name,
diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js
index 563368384b..3dbe2b2b7f 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.js
@@ -21,6 +21,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import Field from "../elements/Field";
 import * as sdk from "../../../index";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 // TODO: Merge with ProfileSettings?
 @replaceableComponent("views.room_settings.RoomProfileSettings")
@@ -38,7 +39,7 @@ export default class RoomProfileSettings extends React.Component {
 
         const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
         let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
-        if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
+        if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96);
 
         const topicEvent = room.currentState.getStateEvents("m.room.topic", "");
         const topic = topicEvent && topicEvent.getContent() ? topicEvent.getContent()['topic'] : '';
@@ -112,7 +113,7 @@ export default class RoomProfileSettings extends React.Component {
         if (this.state.avatarFile) {
             const uri = await client.uploadContent(this.state.avatarFile);
             await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: uri}, '');
-            newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
+            newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
             newState.originalAvatarUrl = newState.avatarUrl;
             newState.avatarFile = null;
         } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js
index 39c9f0bcf7..536abf57fc 100644
--- a/src/components/views/rooms/LinkPreviewWidget.js
+++ b/src/components/views/rooms/LinkPreviewWidget.js
@@ -26,6 +26,7 @@ import Modal from "../../../Modal";
 import * as ImageUtils from "../../../ImageUtils";
 import { _t } from "../../../languageHandler";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 @replaceableComponent("views.rooms.LinkPreviewWidget")
 export default class LinkPreviewWidget extends React.Component {
@@ -83,7 +84,7 @@ export default class LinkPreviewWidget extends React.Component {
 
         let src = p["og:image"];
         if (src && src.startsWith("mxc://")) {
-            src = MatrixClientPeg.get().mxcUrlToHttp(src);
+            src = mediaFromMxc(src).srcHttp;
         }
 
         const params = {
@@ -109,9 +110,11 @@ export default class LinkPreviewWidget extends React.Component {
         if (!SettingsStore.getValue("showImages")) {
             image = null; // Don't render a button to show the image, just hide it outright
         }
-        const imageMaxWidth = 100; const imageMaxHeight = 100;
+        const imageMaxWidth = 100;
+        const imageMaxHeight = 100;
         if (image && image.startsWith("mxc://")) {
-            image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
+            // We deliberately don't want a square here, so use the source HTTP thumbnail function
+            image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, 'scale');
         }
 
         let thumbHeight = imageMaxHeight;
diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js
index 8067046ffd..0b6739df64 100644
--- a/src/components/views/settings/ChangeAvatar.js
+++ b/src/components/views/settings/ChangeAvatar.js
@@ -21,6 +21,7 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import Spinner from '../elements/Spinner';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 @replaceableComponent("views.settings.ChangeAvatar")
 export default class ChangeAvatar extends React.Component {
@@ -117,7 +118,7 @@ export default class ChangeAvatar extends React.Component {
         httpPromise.then(function() {
             self.setState({
                 phase: ChangeAvatar.Phases.Display,
-                avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),
+                avatarUrl: mediaFromMxc(newUrl).srcHttp,
             });
         }, function(error) {
             self.setState({
diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index 30dcdc3c47..971b868751 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -24,6 +24,7 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore";
 import Modal from "../../../Modal";
 import ErrorDialog from "../dialogs/ErrorDialog";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 @replaceableComponent("views.settings.ProfileSettings")
 export default class ProfileSettings extends React.Component {
@@ -32,7 +33,7 @@ export default class ProfileSettings extends React.Component {
 
         const client = MatrixClientPeg.get();
         let avatarUrl = OwnProfileStore.instance.avatarMxc;
-        if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
+        if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96);
         this.state = {
             userId: client.getUserId(),
             originalDisplayName: OwnProfileStore.instance.displayName,
@@ -97,7 +98,7 @@ export default class ProfileSettings extends React.Component {
                     ` (${this.state.avatarFile.size}) bytes`);
                 const uri = await client.uploadContent(this.state.avatarFile);
                 await client.setAvatarUrl(uri);
-                newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
+                newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
                 newState.originalAvatarUrl = newState.avatarUrl;
                 newState.avatarFile = null;
             } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts
index 27abc6bc50..f42307c530 100644
--- a/src/customisations/Media.ts
+++ b/src/customisations/Media.ts
@@ -16,6 +16,7 @@
 
 import {MatrixClientPeg} from "../MatrixClientPeg";
 import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent";
+import {ResizeMode} from "./models/ResizeMode";
 
 // Populate this class with the details of your customisations when copying it.
 
@@ -33,6 +34,13 @@ export class Media {
     constructor(private prepared: IPreparedMedia) {
     }
 
+    /**
+     * True if the media appears to be encrypted. Actual file contents may vary.
+     */
+    public get isEncrypted(): boolean {
+        return !!this.prepared.file;
+    }
+
     /**
      * The MXC URI of the source media.
      */
@@ -62,6 +70,15 @@ export class Media {
         return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc);
     }
 
+    /**
+     * The HTTP URL for the thumbnail media (without any specified width, height, etc). Null/undefined
+     * if no thumbnail media recorded.
+     */
+    public get thumbnailHttp(): string | undefined | null {
+        if (!this.hasThumbnail) return null;
+        return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc);
+    }
+
     /**
      * Gets the HTTP URL for the thumbnail media with the requested characteristics, if a thumbnail
      * is recorded for this media. Returns null/undefined otherwise.
@@ -70,7 +87,7 @@ export class Media {
      * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
      * @returns {string} The HTTP URL which points to the thumbnail.
      */
-    public getThumbnailHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string | null | undefined {
+    public getThumbnailHttp(width: number, height: number, mode: ResizeMode = "scale"): string | null | undefined {
         if (!this.hasThumbnail) return null;
         return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode);
     }
@@ -82,10 +99,23 @@ export class Media {
      * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
      * @returns {string} The HTTP URL which points to the thumbnail.
      */
-    public getThumbnailOfSourceHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string {
+    public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMode = "scale"): string {
         return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode);
     }
 
+    /**
+     * Creates a square thumbnail of the media. If the media has a thumbnail recorded, that MXC will
+     * be used, otherwise the source media will be used.
+     * @param {number} dim The desired width and height.
+     * @returns {string} An HTTP URL for the thumbnail.
+     */
+    public getSquareThumbnailHttp(dim: number): string {
+        if (this.hasThumbnail) {
+            return this.getThumbnailHttp(dim, dim, 'crop');
+        }
+        return this.getThumbnailOfSourceHttp(dim, dim, 'crop');
+    }
+
     /**
      * Downloads the source media.
      * @returns {Promise<Response>} Resolves to the server's response for chaining.
@@ -102,7 +132,7 @@ export class Media {
      * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
      * @returns {Promise<Response>} Resolves to the server's response for chaining.
      */
-    public downloadThumbnail(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise<Response> {
+    public downloadThumbnail(width: number, height: number, mode: ResizeMode = "scale"): Promise<Response> {
         if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail");
         return fetch(this.getThumbnailHttp(width, height, mode));
     }
@@ -114,7 +144,7 @@ export class Media {
      * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
      * @returns {Promise<Response>} Resolves to the server's response for chaining.
      */
-    public downloadThumbnailOfSource(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise<Response> {
+    public downloadThumbnailOfSource(width: number, height: number, mode: ResizeMode = "scale"): Promise<Response> {
         return fetch(this.getThumbnailOfSourceHttp(width, height, mode));
     }
 }
diff --git a/src/customisations/models/ResizeMode.ts b/src/customisations/models/ResizeMode.ts
new file mode 100644
index 0000000000..401b6723e5
--- /dev/null
+++ b/src/customisations/models/ResizeMode.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export type ResizeMode = "scale" | "crop";
diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts
index 8983380fec..5e722877e2 100644
--- a/src/stores/OwnProfileStore.ts
+++ b/src/stores/OwnProfileStore.ts
@@ -22,6 +22,7 @@ import { User } from "matrix-js-sdk/src/models/user";
 import { throttle } from "lodash";
 import { MatrixClientPeg } from "../MatrixClientPeg";
 import { _t } from "../languageHandler";
+import {mediaFromMxc} from "../customisations/Media";
 
 interface IState {
     displayName?: string;
@@ -72,8 +73,12 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
      */
     public getHttpAvatarUrl(size = 0): string {
         if (!this.avatarMxc) return null;
-        const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through
-        return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize);
+        const media = mediaFromMxc(this.avatarMxc);
+        if (!size || size <= 0) {
+            return media.srcHttp;
+        } else {
+            return media.getSquareThumbnailHttp(size);
+        }
     }
 
     protected async onNotReady() {

From fa5d98c319c5cddea8fe94c0cc70215e7909401f Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 18:45:09 -0700
Subject: [PATCH 370/389] Convert cases of getHttpUriForMxc to new media
 customisation

---
 src/Avatar.ts                                    | 10 +++-------
 src/components/structures/RoomDirectory.js       |  9 ++++-----
 src/components/views/avatars/RoomAvatar.tsx      | 16 ++++++++--------
 src/components/views/avatars/WidgetAvatar.tsx    |  4 ++--
 .../dialogs/CommunityPrototypeInviteDialog.tsx   | 10 ++++++----
 src/components/views/dialogs/InviteDialog.tsx    | 14 +++++++-------
 src/components/views/rooms/RoomDetailRow.js      | 10 +++++-----
 src/components/views/settings/BridgeTile.tsx     |  8 ++------
 8 files changed, 37 insertions(+), 44 deletions(-)

diff --git a/src/Avatar.ts b/src/Avatar.ts
index e2557e21a8..eeef3e2c69 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
 import {RoomMember} from "matrix-js-sdk/src/models/room-member";
 import {User} from "matrix-js-sdk/src/models/user";
 import {Room} from "matrix-js-sdk/src/models/room";
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import DMRoomMap from './utils/DMRoomMap';
+import {mediaFromMxc} from "./customisations/Media";
 
 export type ResizeMethod = "crop" | "scale";
 
@@ -47,16 +47,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu
 }
 
 export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
-    const url = getHttpUriForMxc(
-        MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
+    if (!user.avatarUrl) return null;
+    return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(
         Math.floor(width * window.devicePixelRatio),
         Math.floor(height * window.devicePixelRatio),
         resizeMethod,
     );
-    if (!url || url.length === 0) {
-        return null;
-    }
-    return url;
 }
 
 function isValidHexColor(color: string): boolean {
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 363c67262b..3613261da6 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -27,7 +27,6 @@ import { _t } from '../../languageHandler';
 import SdkConfig from '../../SdkConfig';
 import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
 import Analytics from '../../Analytics';
-import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
 import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
 import SettingsStore from "../../settings/SettingsStore";
 import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
@@ -35,6 +34,7 @@ import GroupStore from "../../stores/GroupStore";
 import FlairStore from "../../stores/FlairStore";
 import CountlyAnalytics from "../../CountlyAnalytics";
 import {replaceableComponent} from "../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../customisations/Media";
 
 const MAX_NAME_LENGTH = 80;
 const MAX_TOPIC_LENGTH = 800;
@@ -521,10 +521,9 @@ export default class RoomDirectory extends React.Component {
             topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`;
         }
         topic = linkifyAndSanitizeHtml(topic);
-        const avatarUrl = getHttpUriForMxc(
-                                MatrixClientPeg.get().getHomeserverUrl(),
-                                room.avatar_url, 32, 32, "crop",
-                            );
+        let avatarUrl = null;
+        if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
+
         return [
             <div key={ `${room.room_id}_avatar` }
                 onClick={(ev) => this.onRoomClicked(room, ev)}
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 0a59f6e36a..31245b44b7 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -15,7 +15,6 @@ limitations under the License.
 */
 import React, {ComponentProps} from 'react';
 import Room from 'matrix-js-sdk/src/models/room';
-import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo';
 
 import BaseAvatar from './BaseAvatar';
 import ImageView from '../elements/ImageView';
@@ -24,6 +23,7 @@ import Modal from '../../../Modal';
 import * as Avatar from '../../../Avatar';
 import {ResizeMethod} from "../../../Avatar";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
     // Room may be left unset here, but if it is,
@@ -90,16 +90,16 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
     };
 
     private static getImageUrls(props: IProps): string[] {
-        return [
-            getHttpUriForMxc(
-                MatrixClientPeg.get().getHomeserverUrl(),
-                // Default props don't play nicely with getDerivedStateFromProps
-                //props.oobData !== undefined ? props.oobData.avatarUrl : {},
-                props.oobData.avatarUrl,
+        let oobAvatar = null;
+        if (props.oobData.avatarUrl) {
+            oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp(
                 Math.floor(props.width * window.devicePixelRatio),
                 Math.floor(props.height * window.devicePixelRatio),
                 props.resizeMethod,
-            ), // highest priority
+            );
+        }
+        return [
+            oobAvatar, // highest priority
             RoomAvatar.getRoomAvatarUrl(props),
         ].filter(function(url) {
             return (url !== null && url !== "");
diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx
index 04cfce7670..6468b0dd49 100644
--- a/src/components/views/avatars/WidgetAvatar.tsx
+++ b/src/components/views/avatars/WidgetAvatar.tsx
@@ -16,11 +16,11 @@ limitations under the License.
 
 import React, {ComponentProps, useContext} from 'react';
 import classNames from 'classnames';
-import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
 
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {IApp} from "../../../stores/WidgetStore";
 import BaseAvatar, {BaseAvatarType} from "./BaseAvatar";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "urls"> {
     app: IApp;
@@ -47,7 +47,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
             name={app.id}
             className={classNames("mx_WidgetAvatar", className)}
             // MSC2765
-            url={app.avatar_url ? getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop") : undefined}
+            url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : undefined}
             urls={iconUrls}
             width={width}
             height={height}
diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
index d1080566ac..2635f95bb7 100644
--- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
+++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
@@ -26,12 +26,12 @@ import SdkConfig from "../../../SdkConfig";
 import { RoomMember } from "matrix-js-sdk/src/models/room-member";
 import InviteDialog from "./InviteDialog";
 import BaseAvatar from "../avatars/BaseAvatar";
-import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
 import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
 import StyledCheckbox from "../elements/StyledCheckbox";
 import Modal from "../../../Modal";
 import ErrorDialog from "./ErrorDialog";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 interface IProps extends IDialogProps {
     roomId: string;
@@ -142,12 +142,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
 
     private renderPerson(person: IPerson, key: any) {
         const avatarSize = 36;
+        let avatarUrl = null;
+        if (person.user.getMxcAvatarUrl()) {
+            avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize);
+        }
         return (
             <div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
                 <BaseAvatar
-                    url={getHttpUriForMxc(
-                        MatrixClientPeg.get().getHomeserverUrl(), person.user.getMxcAvatarUrl(),
-                        avatarSize, avatarSize, "crop")}
+                    url={avatarUrl}
                     name={person.user.name}
                     idName={person.user.userId}
                     width={avatarSize}
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index b28cc1bf41..9aef421d5a 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -22,7 +22,6 @@ import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Pe
 import DMRoomMap from "../../../utils/DMRoomMap";
 import {RoomMember} from "matrix-js-sdk/src/models/room-member";
 import SdkConfig from "../../../SdkConfig";
-import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
 import * as Email from "../../../email";
 import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
 import {abbreviateUrl} from "../../../utils/UrlUtils";
@@ -43,6 +42,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics";
 import {Room} from "matrix-js-sdk/src/models/room";
 import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
 /* eslint-disable camelcase */
@@ -160,9 +160,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
                 width={avatarSize} height={avatarSize} />
             : <BaseAvatar
                 className='mx_InviteDialog_userTile_avatar'
-                url={getHttpUriForMxc(
-                    MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
-                    avatarSize, avatarSize, "crop")}
+                url={this.props.member.getMxcAvatarUrl()
+                    ? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
+                    : null}
                 name={this.props.member.name}
                 idName={this.props.member.userId}
                 width={avatarSize}
@@ -262,9 +262,9 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
                 src={require("../../../../res/img/icon-email-pill-avatar.svg")}
                 width={avatarSize} height={avatarSize} />
             : <BaseAvatar
-                url={getHttpUriForMxc(
-                    MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
-                    avatarSize, avatarSize, "crop")}
+                url={this.props.member.getMxcAvatarUrl()
+                    ? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
+                    : null}
                 name={this.props.member.name}
                 idName={this.props.member.userId}
                 width={avatarSize}
diff --git a/src/components/views/rooms/RoomDetailRow.js b/src/components/views/rooms/RoomDetailRow.js
index e7c259cd98..62960930f2 100644
--- a/src/components/views/rooms/RoomDetailRow.js
+++ b/src/components/views/rooms/RoomDetailRow.js
@@ -18,10 +18,9 @@ import * as sdk from '../../../index';
 import React, {createRef} from 'react';
 import { _t } from '../../../languageHandler';
 import { linkifyElement } from '../../../HtmlUtils';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import PropTypes from 'prop-types';
-import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 export function getDisplayAliasForRoom(room) {
     return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
@@ -100,13 +99,14 @@ export default class RoomDetailRow extends React.Component {
             { guestJoin }
         </div>) : <div />;
 
+        let avatarUrl = null;
+        if (room.avatarUrl) avatarUrl = mediaFromMxc(room.avatarUrl).getSquareThumbnailHttp(24);
+
         return <tr key={room.roomId} onClick={this.onClick} onMouseDown={this.props.onMouseDown}>
             <td className="mx_RoomDirectory_roomAvatar">
                 <BaseAvatar width={24} height={24} resizeMethod='crop'
                     name={name} idName={name}
-                    url={getHttpUriForMxc(
-                            MatrixClientPeg.get().getHomeserverUrl(),
-                            room.avatarUrl, 24, 24, "crop")} />
+                    url={avatarUrl} />
             </td>
             <td className="mx_RoomDirectory_roomDescription">
                 <div className="mx_RoomDirectory_name">{ name }</div>&nbsp;
diff --git a/src/components/views/settings/BridgeTile.tsx b/src/components/views/settings/BridgeTile.tsx
index b33219ad4a..3565d1ba2e 100644
--- a/src/components/views/settings/BridgeTile.tsx
+++ b/src/components/views/settings/BridgeTile.tsx
@@ -16,9 +16,7 @@ limitations under the License.
 
 import React from 'react';
 import PropTypes from 'prop-types';
-import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
 import {_t} from "../../../languageHandler";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import Pill from "../elements/Pill";
 import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
 import BaseAvatar from "../avatars/BaseAvatar";
@@ -27,6 +25,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event";
 import { Room } from "matrix-js-sdk/src/models/room";
 import { isUrlPermitted } from '../../../HtmlUtils';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 
 interface IProps {
     ev: MatrixEvent;
@@ -114,10 +113,7 @@ export default class BridgeTile extends React.PureComponent<IProps> {
         let networkIcon;
 
         if (protocol.avatar_url) {
-            const avatarUrl = getHttpUriForMxc(
-                MatrixClientPeg.get().getHomeserverUrl(),
-                protocol.avatar_url, 64, 64, "crop",
-            );
+            const avatarUrl = mediaFromMxc(protocol.avatar_url).getSquareThumbnailHttp(64);
 
             networkIcon = <BaseAvatar className="protocol-icon"
                 width={48}

From 533c9fed647772d561ccd58bc1e987c91a901b2d Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 5 Mar 2021 18:49:32 -0700
Subject: [PATCH 371/389] Appease the linter

---
 src/HtmlUtils.tsx                               |  1 -
 src/components/structures/LeftPanel.tsx         |  1 -
 src/components/views/avatars/GroupAvatar.tsx    |  1 -
 src/components/views/avatars/WidgetAvatar.tsx   |  5 +----
 src/components/views/elements/AddressTile.js    |  1 -
 src/components/views/elements/Pill.js           | 10 ++++------
 src/components/views/messages/MAudioBody.js     |  1 -
 src/components/views/messages/MFileBody.js      |  1 -
 src/components/views/messages/MVideoBody.tsx    |  1 -
 src/components/views/right_panel/UserInfo.tsx   |  2 +-
 src/customisations/models/IMediaEventContent.ts |  6 +++---
 test/components/structures/GroupView-test.js    |  3 ++-
 12 files changed, 11 insertions(+), 22 deletions(-)

diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 12752eb20f..59b596a5da 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -32,7 +32,6 @@ import { AllHtmlEntities } from 'html-entities';
 import SettingsStore from './settings/SettingsStore';
 import cheerio from 'cheerio';
 
-import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
 import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
 import ReplyThread from "./components/views/elements/ReplyThread";
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index f7865d094a..9a1ce63785 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -36,7 +36,6 @@ import {Key} from "../../Keyboard";
 import IndicatorScrollbar from "../structures/IndicatorScrollbar";
 import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
 import { OwnProfileStore } from "../../stores/OwnProfileStore";
-import { MatrixClientPeg } from "../../MatrixClientPeg";
 import RoomListNumResults from "../views/rooms/RoomListNumResults";
 import LeftPanelWidget from "./LeftPanelWidget";
 import SpacePanel from "../views/spaces/SpacePanel";
diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx
index dc363da304..321ca025a3 100644
--- a/src/components/views/avatars/GroupAvatar.tsx
+++ b/src/components/views/avatars/GroupAvatar.tsx
@@ -15,7 +15,6 @@ limitations under the License.
 */
 
 import React from 'react';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import BaseAvatar from './BaseAvatar';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 import {mediaFromMxc} from "../../../customisations/Media";
diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx
index 6468b0dd49..cca158269e 100644
--- a/src/components/views/avatars/WidgetAvatar.tsx
+++ b/src/components/views/avatars/WidgetAvatar.tsx
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {ComponentProps, useContext} from 'react';
+import React, {ComponentProps} from 'react';
 import classNames from 'classnames';
 
-import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {IApp} from "../../../stores/WidgetStore";
 import BaseAvatar, {BaseAvatarType} from "./BaseAvatar";
 import {mediaFromMxc} from "../../../customisations/Media";
@@ -27,8 +26,6 @@ interface IProps extends Omit<ComponentProps<BaseAvatarType>, "name" | "url" | "
 }
 
 const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 20, ...props }) => {
-    const cli = useContext(MatrixClientContext);
-
     let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
     // heuristics for some better icons until Widgets support their own icons
     if (app.type.includes("jitsi")) {
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js
index 4f5ee45a3c..df66d10a71 100644
--- a/src/components/views/elements/AddressTile.js
+++ b/src/components/views/elements/AddressTile.js
@@ -19,7 +19,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import * as sdk from "../../../index";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import { _t } from '../../../languageHandler';
 import { UserAddressType } from '../../../UserAddress.js';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index bf99ee6078..e61d312305 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -1,7 +1,5 @@
 /*
-Copyright 2017 Vector Creations Ltd
-Copyright 2018 New Vector Ltd
-Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
+Copyright 2017 - 2019, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -255,12 +253,12 @@ class Pill extends React.Component {
             case Pill.TYPE_GROUP_MENTION: {
                 if (this.state.group) {
                     const {avatarUrl, groupId, name} = this.state.group;
-                    const cli = MatrixClientPeg.get();
 
                     linkText = groupId;
                     if (this.props.shouldShowPillAvatar) {
-                        avatar = <BaseAvatar name={name || groupId} width={16} height={16} aria-hidden="true"
-                                             url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
+                        avatar = <BaseAvatar
+                            name={name || groupId} width={16} height={16} aria-hidden="true"
+                            url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
                     }
                     pillClass = 'mx_GroupPill';
                 }
diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js
index 78ded9a514..0d5e449fc0 100644
--- a/src/components/views/messages/MAudioBody.js
+++ b/src/components/views/messages/MAudioBody.js
@@ -17,7 +17,6 @@
 import React from 'react';
 import MFileBody from './MFileBody';
 
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { decryptFile } from '../../../utils/DecryptFile';
 import { _t } from '../../../languageHandler';
 import InlineSpinner from '../elements/InlineSpinner';
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index 07d7beb793..39a03a1304 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -18,7 +18,6 @@ limitations under the License.
 import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
 import filesize from 'filesize';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import {decryptFile} from '../../../utils/DecryptFile';
diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx
index 32b071ea24..89e661cb2f 100644
--- a/src/components/views/messages/MVideoBody.tsx
+++ b/src/components/views/messages/MVideoBody.tsx
@@ -17,7 +17,6 @@ limitations under the License.
 
 import React from 'react';
 import MFileBody from './MFileBody';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import { decryptFile } from '../../../utils/DecryptFile';
 import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index d415d19852..aa11b70e50 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -1416,7 +1416,7 @@ const UserInfoHeader: React.FC<{
         };
 
         Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
-    }, [cli, member]);
+    }, [member]);
 
     const avatarElement = (
         <div className="mx_UserInfo_avatar">
diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts
index 4cbe07dbd5..fb05d76a4d 100644
--- a/src/customisations/models/IMediaEventContent.ts
+++ b/src/customisations/models/IMediaEventContent.ts
@@ -21,7 +21,7 @@ export interface IEncryptedFile {
     mimetype?: string;
     key: {
         alg: string;
-        key_ops: string[];
+        key_ops: string[]; // eslint-disable-line camelcase
         kty: string;
         k: string;
         ext: boolean;
@@ -35,8 +35,8 @@ export interface IMediaEventContent {
     url?: string; // required on unencrypted media
     file?: IEncryptedFile; // required for *encrypted* media
     info?: {
-        thumbnail_url?: string;
-        thumbnail_file?: IEncryptedFile;
+        thumbnail_url?: string; // eslint-disable-line camelcase
+        thumbnail_file?: IEncryptedFile; // eslint-disable-line camelcase
     };
 }
 
diff --git a/test/components/structures/GroupView-test.js b/test/components/structures/GroupView-test.js
index fb942d2f7c..ee5d1b6912 100644
--- a/test/components/structures/GroupView-test.js
+++ b/test/components/structures/GroupView-test.js
@@ -262,7 +262,8 @@ describe('GroupView', function() {
             expect(longDescElement.innerHTML).toContain('<ul>');
             expect(longDescElement.innerHTML).toContain('<li>And lists!</li>');
 
-            const imgSrc = "https://my.home.server/_matrix/media/r0/thumbnail/someimageurl?width=800&amp;height=600";
+            const imgSrc = "https://my.home.server/_matrix/media/r0/thumbnail/someimageurl" +
+                "?width=800&amp;height=600&amp;method=scale";
             expect(longDescElement.innerHTML).toContain('<img src="' + imgSrc + '">');
         });
 

From 2a40bc87cc602ffd569433d1f8c5185a37eaad31 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 17:03:14 -0700
Subject: [PATCH 372/389] Fix ResizeMethod usage

---
 src/components/views/avatars/BaseAvatar.tsx   |  3 ++-
 src/components/views/avatars/GroupAvatar.tsx  |  4 ++--
 src/components/views/avatars/MemberAvatar.tsx |  3 ++-
 src/customisations/Media.ts                   | 10 +++++-----
 src/customisations/models/ResizeMode.ts       | 17 -----------------
 5 files changed, 11 insertions(+), 26 deletions(-)
 delete mode 100644 src/customisations/models/ResizeMode.ts

diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx
index 799a559263..e623439174 100644
--- a/src/components/views/avatars/BaseAvatar.tsx
+++ b/src/components/views/avatars/BaseAvatar.tsx
@@ -25,6 +25,7 @@ import AccessibleButton from '../elements/AccessibleButton';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {useEventEmitter} from "../../../hooks/useEventEmitter";
 import {toPx} from "../../../utils/units";
+import {ResizeMethod} from "../../../Avatar";
 
 interface IProps {
     name: string; // The name (first initial used as default)
@@ -35,7 +36,7 @@ interface IProps {
     width?: number;
     height?: number;
     // XXX: resizeMethod not actually used.
-    resizeMethod?: string;
+    resizeMethod?: ResizeMethod;
     defaultToInitialLetter?: boolean; // true to add default url
     onClick?: React.MouseEventHandler;
     inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx
index 321ca025a3..6d8ef9e8f6 100644
--- a/src/components/views/avatars/GroupAvatar.tsx
+++ b/src/components/views/avatars/GroupAvatar.tsx
@@ -18,7 +18,7 @@ import React from 'react';
 import BaseAvatar from './BaseAvatar';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 import {mediaFromMxc} from "../../../customisations/Media";
-import {ResizeMode} from "../../../customisations/models/ResizeMode";
+import {ResizeMethod} from "../../../Avatar";
 
 export interface IProps {
         groupId?: string;
@@ -26,7 +26,7 @@ export interface IProps {
         groupAvatarUrl?: string;
         width?: number;
         height?: number;
-        resizeMethod?: ResizeMode;
+        resizeMethod?: ResizeMethod;
         onClick?: React.MouseEventHandler;
 }
 
diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index 641046aa55..5a866f91fe 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -23,13 +23,14 @@ import {Action} from "../../../dispatcher/actions";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import BaseAvatar from "./BaseAvatar";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {ResizeMethod} from "../../../Avatar";
 
 interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
     member: RoomMember;
     fallbackUserId?: string;
     width: number;
     height: number;
-    resizeMethod?: string;
+    resizeMethod?: ResizeMethod;
     // The onClick to give the avatar
     onClick?: React.MouseEventHandler;
     // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts
index f42307c530..812dd974a9 100644
--- a/src/customisations/Media.ts
+++ b/src/customisations/Media.ts
@@ -16,7 +16,7 @@
 
 import {MatrixClientPeg} from "../MatrixClientPeg";
 import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent";
-import {ResizeMode} from "./models/ResizeMode";
+import {ResizeMethod} from "../Avatar";
 
 // Populate this class with the details of your customisations when copying it.
 
@@ -87,7 +87,7 @@ export class Media {
      * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
      * @returns {string} The HTTP URL which points to the thumbnail.
      */
-    public getThumbnailHttp(width: number, height: number, mode: ResizeMode = "scale"): string | null | undefined {
+    public getThumbnailHttp(width: number, height: number, mode: ResizeMethod = "scale"): string | null | undefined {
         if (!this.hasThumbnail) return null;
         return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode);
     }
@@ -99,7 +99,7 @@ export class Media {
      * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
      * @returns {string} The HTTP URL which points to the thumbnail.
      */
-    public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMode = "scale"): string {
+    public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMethod = "scale"): string {
         return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode);
     }
 
@@ -132,7 +132,7 @@ export class Media {
      * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
      * @returns {Promise<Response>} Resolves to the server's response for chaining.
      */
-    public downloadThumbnail(width: number, height: number, mode: ResizeMode = "scale"): Promise<Response> {
+    public downloadThumbnail(width: number, height: number, mode: ResizeMethod = "scale"): Promise<Response> {
         if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail");
         return fetch(this.getThumbnailHttp(width, height, mode));
     }
@@ -144,7 +144,7 @@ export class Media {
      * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
      * @returns {Promise<Response>} Resolves to the server's response for chaining.
      */
-    public downloadThumbnailOfSource(width: number, height: number, mode: ResizeMode = "scale"): Promise<Response> {
+    public downloadThumbnailOfSource(width: number, height: number, mode: ResizeMethod = "scale"): Promise<Response> {
         return fetch(this.getThumbnailOfSourceHttp(width, height, mode));
     }
 }
diff --git a/src/customisations/models/ResizeMode.ts b/src/customisations/models/ResizeMode.ts
deleted file mode 100644
index 401b6723e5..0000000000
--- a/src/customisations/models/ResizeMode.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright 2021 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *         http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export type ResizeMode = "scale" | "crop";

From 4688c887c4a6fa08394e666826481c74b5a17f02 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 17:03:29 -0700
Subject: [PATCH 373/389] Fix GroupAvatar crash

When we don't have an avatar we shouldn't explode.
---
 src/components/views/avatars/GroupAvatar.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx
index 6d8ef9e8f6..3734ba9504 100644
--- a/src/components/views/avatars/GroupAvatar.tsx
+++ b/src/components/views/avatars/GroupAvatar.tsx
@@ -39,6 +39,7 @@ export default class GroupAvatar extends React.Component<IProps> {
     };
 
     getGroupAvatarUrl() {
+        if (!this.props.groupAvatarUrl) return null;
         return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp(
             this.props.width,
             this.props.height,

From 125aef24b99508bfa8248f8bc4c85a4ed05e7e61 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Mon, 8 Mar 2021 17:04:00 -0700
Subject: [PATCH 374/389] Convert edge cases of MXC URI conversion to new
 customisation endpoint

---
 src/Avatar.ts                                 | 29 ++++---------------
 src/components/views/avatars/MemberAvatar.tsx | 19 ++++++------
 2 files changed, 16 insertions(+), 32 deletions(-)

diff --git a/src/Avatar.ts b/src/Avatar.ts
index eeef3e2c69..76c88faa1c 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -18,7 +18,6 @@ import {RoomMember} from "matrix-js-sdk/src/models/room-member";
 import {User} from "matrix-js-sdk/src/models/user";
 import {Room} from "matrix-js-sdk/src/models/room";
 
-import {MatrixClientPeg} from './MatrixClientPeg';
 import DMRoomMap from './utils/DMRoomMap';
 import {mediaFromMxc} from "./customisations/Media";
 
@@ -27,14 +26,11 @@ export type ResizeMethod = "crop" | "scale";
 // Not to be used for BaseAvatar urls as that has similar default avatar fallback already
 export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
     let url: string;
-    if (member && member.getAvatarUrl) {
-        url = member.getAvatarUrl(
-            MatrixClientPeg.get().getHomeserverUrl(),
+    if (member?.getMxcAvatarUrl()) {
+        url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
             Math.floor(width * window.devicePixelRatio),
             Math.floor(height * window.devicePixelRatio),
             resizeMethod,
-            false,
-            false,
         );
     }
     if (!url) {
@@ -150,15 +146,8 @@ export function getInitialLetter(name: string): string {
 export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
     if (!room) return null; // null-guard
 
-    const explicitRoomAvatar = room.getAvatarUrl(
-        MatrixClientPeg.get().getHomeserverUrl(),
-        width,
-        height,
-        resizeMethod,
-        false,
-    );
-    if (explicitRoomAvatar) {
-        return explicitRoomAvatar;
+    if (room.getMxcAvatarUrl()) {
+        return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
     }
 
     // space rooms cannot be DMs so skip the rest
@@ -173,14 +162,8 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
         // then still try to show any avatar (pref. other member)
         otherMember = room.getAvatarFallbackMember();
     }
-    if (otherMember) {
-        return otherMember.getAvatarUrl(
-            MatrixClientPeg.get().getHomeserverUrl(),
-            width,
-            height,
-            resizeMethod,
-            false,
-        );
+    if (otherMember?.getMxcAvatarUrl()) {
+        return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
     }
     return null;
 }
diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index 5a866f91fe..3483802af5 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -20,9 +20,9 @@ import {RoomMember} from "matrix-js-sdk/src/models/room-member";
 
 import dis from "../../../dispatcher/dispatcher";
 import {Action} from "../../../dispatcher/actions";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import BaseAvatar from "./BaseAvatar";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {mediaFromMxc} from "../../../customisations/Media";
 import {ResizeMethod} from "../../../Avatar";
 
 interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
@@ -65,17 +65,18 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
 
     private static getState(props: IProps): IState {
         if (props.member && props.member.name) {
-            return {
-                name: props.member.name,
-                title: props.title || props.member.userId,
-                imageUrl: props.member.getAvatarUrl(
-                    MatrixClientPeg.get().getHomeserverUrl(),
+            let imageUrl = null;
+            if (props.member.getMxcAvatarUrl()) {
+                imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
                     Math.floor(props.width * window.devicePixelRatio),
                     Math.floor(props.height * window.devicePixelRatio),
                     props.resizeMethod,
-                    false,
-                    false,
-                ),
+                );
+            }
+            return {
+                name: props.member.name,
+                title: props.title || props.member.userId,
+                imageUrl: imageUrl,
             };
         } else if (props.fallbackUserId) {
             return {

From af9f17219b66c96a56f8e63e87c8ce15e25129cd Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 9 Mar 2021 12:41:36 -0700
Subject: [PATCH 375/389] Fix MemberAvatar crash from EventTilePreview

---
 src/components/views/avatars/MemberAvatar.tsx      | 2 +-
 src/components/views/elements/EventTilePreview.tsx | 7 +++----
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx
index 3483802af5..c79cbc0d32 100644
--- a/src/components/views/avatars/MemberAvatar.tsx
+++ b/src/components/views/avatars/MemberAvatar.tsx
@@ -64,7 +64,7 @@ export default class MemberAvatar extends React.Component<IProps, IState> {
     }
 
     private static getState(props: IProps): IState {
-        if (props.member && props.member.name) {
+        if (props.member?.name) {
             let imageUrl = null;
             if (props.member.getMxcAvatarUrl()) {
                 imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index c539f2be1c..5fd73f974d 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -70,9 +70,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
         const client = MatrixClientPeg.get();
         const userId = client.getUserId();
         const profileInfo = await client.getProfileInfo(userId);
-        const avatarUrl = Avatar.avatarUrlForUser(
-            {avatarUrl: profileInfo.avatar_url},
-            AVATAR_SIZE, AVATAR_SIZE, "crop");
+        const avatarUrl = profileInfo.avatar_url;
 
         this.setState({
             userId,
@@ -113,8 +111,9 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
             name: displayname,
             userId: userId,
             getAvatarUrl: (..._) => {
-                return avatarUrl;
+                return Avatar.avatarUrlForUser({avatarUrl}, AVATAR_SIZE, AVATAR_SIZE, "crop");
             },
+            getMxcAvatarUrl: () => avatarUrl,
         };
 
         return event;

From a7debdd94658b09c9a4b0f6854b46fdc9752616e Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 9 Mar 2021 12:54:20 -0700
Subject: [PATCH 376/389] Fix tinting for download icon

As shown in https://github.com/vector-im/element-web/issues/16546
---
 res/css/views/messages/_MFileBody.scss     | 15 +++-
 src/components/views/messages/MFileBody.js | 83 ++++------------------
 src/usercontent/index.js                   | 31 ++++----
 3 files changed, 42 insertions(+), 87 deletions(-)

diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss
index e219c0c5e4..b45126acf8 100644
--- a/res/css/views/messages/_MFileBody.scss
+++ b/res/css/views/messages/_MFileBody.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015, 2016, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,6 +16,19 @@ limitations under the License.
 
 .mx_MFileBody_download {
     color: $accent-color;
+
+    .mx_MFileBody_download_icon {
+        // 12px instead of 14px to better match surrounding font size
+        width: 12px;
+        height: 12px;
+        mask-size: 12px;
+
+        mask-position: center;
+        mask-repeat: no-repeat;
+        mask-image: url("$(res)/img/download.svg");
+        background-color: $accent-color;
+        display: inline-block;
+    }
 }
 
 .mx_MFileBody_download a {
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index 39a03a1304..a9bedd1927 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -1,6 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2018 New Vector Ltd
+Copyright 2015, 2016, 2018, 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,52 +17,24 @@ limitations under the License.
 import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
 import filesize from 'filesize';
-import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import {decryptFile} from '../../../utils/DecryptFile';
-import Tinter from '../../../Tinter';
-import request from 'browser-request';
 import Modal from '../../../Modal';
 import AccessibleButton from "../elements/AccessibleButton";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 import {mediaFromContent} from "../../../customisations/Media";
+import ErrorDialog from "../dialogs/ErrorDialog";
 
+let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on
 
-// A cached tinted copy of require("../../../../res/img/download.svg")
-let tintedDownloadImageURL;
-// Track a list of mounted MFileBody instances so that we can update
-// the require("../../../../res/img/download.svg") when the tint changes.
-let nextMountId = 0;
-const mounts = {};
-
-/**
- * Updates the tinted copy of require("../../../../res/img/download.svg") when the tint changes.
- */
-function updateTintedDownloadImage() {
-    // Download the svg as an XML document.
-    // We could cache the XML response here, but since the tint rarely changes
-    // it's probably not worth it.
-    // Also note that we can't use fetch here because fetch doesn't support
-    // file URLs, which the download image will be if we're running from
-    // the filesystem (like in an Electron wrapper).
-    request({uri: require("../../../../res/img/download.svg")}, (err, response, body) => {
-        if (err) return;
-
-        const svg = new DOMParser().parseFromString(body, "image/svg+xml");
-        // Apply the fixups to the XML.
-        const fixups = Tinter.calcSvgFixups([{contentDocument: svg}]);
-        Tinter.applySvgFixups(fixups);
-        // Encoded the fixed up SVG as a data URL.
-        const svgString = new XMLSerializer().serializeToString(svg);
-        tintedDownloadImageURL = "data:image/svg+xml;base64," + window.btoa(svgString);
-        // Notify each mounted MFileBody that the URL has changed.
-        Object.keys(mounts).forEach(function(id) {
-            mounts[id].tint();
-        });
-    });
+async function cacheDownloadIcon() {
+    if (downloadIconUrl) return; // cached already
+    const svg = await fetch(require("../../../../res/img/download.svg")).then(r => r.text());
+    downloadIconUrl = "data:image/svg+xml;base64," + window.btoa(svg);
 }
 
-Tinter.registerTintable(updateTintedDownloadImage);
+// Cache the asset immediately
+cacheDownloadIcon();
 
 // User supplied content can contain scripts, we have to be careful that
 // we don't accidentally run those script within the same origin as the
@@ -106,6 +77,7 @@ function computedStyle(element) {
     }
     const style = window.getComputedStyle(element, null);
     let cssText = style.cssText;
+    // noinspection EqualityComparisonWithCoercionJS
     if (cssText == "") {
         // Firefox doesn't implement ".cssText" for computed styles.
         // https://bugzilla.mozilla.org/show_bug.cgi?id=137687
@@ -145,7 +117,6 @@ export default class MFileBody extends React.Component {
 
         this._iframe = createRef();
         this._dummyLink = createRef();
-        this._downloadImage = createRef();
     }
 
     /**
@@ -182,48 +153,18 @@ export default class MFileBody extends React.Component {
         return media.srcHttp;
     }
 
-    componentDidMount() {
-        // Add this to the list of mounted components to receive notifications
-        // when the tint changes.
-        this.id = nextMountId++;
-        mounts[this.id] = this;
-        this.tint();
-    }
-
     componentDidUpdate(prevProps, prevState) {
         if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
             this.props.onHeightChanged();
         }
     }
 
-    componentWillUnmount() {
-        // Remove this from the list of mounted components
-        delete mounts[this.id];
-    }
-
-    tint = () => {
-        // Update our tinted copy of require("../../../../res/img/download.svg")
-        if (this._downloadImage.current) {
-            this._downloadImage.current.src = tintedDownloadImageURL;
-        }
-        if (this._iframe.current) {
-            // If the attachment is encrypted then the download image
-            // will be inside the iframe so we wont be able to update
-            // it directly.
-            this._iframe.current.contentWindow.postMessage({
-                imgSrc: tintedDownloadImageURL,
-                style: computedStyle(this._dummyLink.current),
-            }, "*");
-        }
-    };
-
     render() {
         const content = this.props.mxEvent.getContent();
         const text = this.presentableTextForFile(content);
         const isEncrypted = content.file !== undefined;
         const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
         const contentUrl = this._getContentUrl();
-        const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
         const fileSize = content.info ? content.info.size : null;
         const fileType = content.info ? content.info.mimetype : "application/octet-stream";
 
@@ -280,7 +221,7 @@ export default class MFileBody extends React.Component {
             // When the iframe loads we tell it to render a download link
             const onIframeLoad = (ev) => {
                 ev.target.contentWindow.postMessage({
-                    imgSrc: tintedDownloadImageURL,
+                    imgSrc: downloadIconUrl,
                     style: computedStyle(this._dummyLink.current),
                     blob: this.state.decryptedBlob,
                     // Set a download attribute for encrypted files so that the file
@@ -384,7 +325,7 @@ export default class MFileBody extends React.Component {
                         {placeholder}
                         <div className="mx_MFileBody_download">
                             <a {...downloadProps}>
-                                <img src={tintedDownloadImageURL} width="12" height="14" ref={this._downloadImage} />
+                                <span className="mx_MFileBody_download_icon" />
                                 { _t("Download %(text)s", { text: text }) }
                             </a>
                         </div>
diff --git a/src/usercontent/index.js b/src/usercontent/index.js
index 6ecd17dcd7..4850dba9b9 100644
--- a/src/usercontent/index.js
+++ b/src/usercontent/index.js
@@ -1,10 +1,8 @@
 function remoteRender(event) {
     const data = event.data;
 
-    const img = document.createElement("img");
+    const img = document.createElement("span"); // we'll mask it as an image
     img.id = "img";
-    img.src = data.imgSrc;
-    img.style = data.imgStyle;
 
     const a = document.createElement("a");
     a.id = "a";
@@ -16,6 +14,21 @@ function remoteRender(event) {
     a.appendChild(img);
     a.appendChild(document.createTextNode(data.textContent));
 
+    // Apply image style after so we can steal the anchor's colour.
+    // Style copied from a rendered version of mx_MFileBody_download_icon
+    img.style = "" +
+        "width: 12px; height: 12px;" +
+        "-webkit-mask-size: 12px;" +
+        "mask-size: 12px;" +
+        "-webkit-mask-position: center;" +
+        "mask-position: center;" +
+        "-webkit-mask-repeat: no-repeat;" +
+        "mask-repeat: no-repeat;" +
+        `-webkit-mask-image: url('${data.imgSrc}');` +
+        `mask-image: url('${data.imgSrc}');` +
+        `background-color: ${a.style.color};` +
+        "display: inline-block;";
+
     const body = document.body;
     // Don't display scrollbars if the link takes more than one line to display.
     body.style = "margin: 0px; overflow: hidden";
@@ -26,20 +39,8 @@ function remoteRender(event) {
     }
 }
 
-function remoteSetTint(event) {
-    const data = event.data;
-
-    const img = document.getElementById("img");
-    img.src = data.imgSrc;
-    img.style = data.imgStyle;
-
-    const a = document.getElementById("a");
-    a.style = data.style;
-}
-
 window.onmessage = function(e) {
     if (e.origin === window.location.origin) {
         if (e.data.blob) remoteRender(e);
-        else remoteSetTint(e);
     }
 };

From ebf1efe01b816eac3f5fa36a58ac671b54ebd5b4 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 9 Mar 2021 14:16:15 -0700
Subject: [PATCH 377/389] docs

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

diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index 0a1f875935..3683818027 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -71,7 +71,7 @@ export default class MImageBody extends React.Component {
         this._image = createRef();
     }
 
-    // FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too!
+    // FIXME: factor this out and apply it to MVideoBody and MAudioBody too!
     onClientSync(syncState, prevState) {
         if (this.unmounted) return;
         // Consider the client reconnected if there is no error with syncing.

From 4ebd35f8453108e87d554bfd787af4e5f210e387 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Tue, 9 Mar 2021 14:59:17 -0700
Subject: [PATCH 378/389] Remove unused functions

---
 src/customisations/Media.ts | 24 ------------------------
 1 file changed, 24 deletions(-)

diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts
index 812dd974a9..f262179f3d 100644
--- a/src/customisations/Media.ts
+++ b/src/customisations/Media.ts
@@ -123,30 +123,6 @@ export class Media {
     public downloadSource(): Promise<Response> {
         return fetch(this.srcHttp);
     }
-
-    /**
-     * Downloads the thumbnail media with the requested characteristics. If no thumbnail media is present,
-     * this throws an exception.
-     * @param {number} width The desired width of the thumbnail.
-     * @param {number} height The desired height of the thumbnail.
-     * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
-     * @returns {Promise<Response>} Resolves to the server's response for chaining.
-     */
-    public downloadThumbnail(width: number, height: number, mode: ResizeMethod = "scale"): Promise<Response> {
-        if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail");
-        return fetch(this.getThumbnailHttp(width, height, mode));
-    }
-
-    /**
-     * Downloads a thumbnail of the source media with the requested characteristics.
-     * @param {number} width The desired width of the thumbnail.
-     * @param {number} height The desired height of the thumbnail.
-     * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale.
-     * @returns {Promise<Response>} Resolves to the server's response for chaining.
-     */
-    public downloadThumbnailOfSource(width: number, height: number, mode: ResizeMethod = "scale"): Promise<Response> {
-        return fetch(this.getThumbnailOfSourceHttp(width, height, mode));
-    }
 }
 
 /**

From 88a2bdb119d721ccdfbb19a9590f09545f858069 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 10 Mar 2021 16:47:27 -0700
Subject: [PATCH 379/389] Change read receipt drift to be non-fractional

I suspect this is what is causing issues in Firefox for read receipts not falling down.
---
 src/components/views/rooms/ReadReceiptMarker.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js
index ba2b3064fd..0000d767df 100644
--- a/src/components/views/rooms/ReadReceiptMarker.js
+++ b/src/components/views/rooms/ReadReceiptMarker.js
@@ -156,14 +156,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
         // then shift to the rightmost column,
         // and then it will drop down to its resting position
         //
-        // XXX: We use a fractional left value to trick velocity-animate into actually animating.
+        // XXX: We use a small left value to trick velocity-animate into actually animating.
         // This is a very annoying bug where if it thinks there's no change to `left` then it'll
         // skip applying it, thus making our read receipt at +14px instead of +0px like it
         // should be. This does cause a tiny amount of drift for read receipts, however with a
         // value so small it's not perceived by a user.
         // Note: Any smaller values (or trying to interchange units) might cause read receipts to
         // fail to fall down or cause gaps.
-        startStyles.push({ top: startTopOffset+'px', left: '0.001px' });
+        startStyles.push({ top: startTopOffset+'px', left: '1px' });
         enterTransitionOpts.push({
             duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
             easing: bounce ? 'easeOutBounce' : 'easeOutCubic',

From 7f52e787482fd20959cfa54b7ce4165ceced7dda Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Wed, 10 Mar 2021 21:08:01 -0700
Subject: [PATCH 380/389] Make styles dynamic

---
 src/components/views/messages/MFileBody.js | 1 +
 src/usercontent/index.js                   | 8 +++++---
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index a9bedd1927..8f464e08bd 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -222,6 +222,7 @@ export default class MFileBody extends React.Component {
             const onIframeLoad = (ev) => {
                 ev.target.contentWindow.postMessage({
                     imgSrc: downloadIconUrl,
+                    imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon.
                     style: computedStyle(this._dummyLink.current),
                     blob: this.state.decryptedBlob,
                     // Set a download attribute for encrypted files so that the file
diff --git a/src/usercontent/index.js b/src/usercontent/index.js
index 4850dba9b9..13f38cc31a 100644
--- a/src/usercontent/index.js
+++ b/src/usercontent/index.js
@@ -16,7 +16,7 @@ function remoteRender(event) {
 
     // Apply image style after so we can steal the anchor's colour.
     // Style copied from a rendered version of mx_MFileBody_download_icon
-    img.style = "" +
+    img.style = (data.imgStyle || "" +
         "width: 12px; height: 12px;" +
         "-webkit-mask-size: 12px;" +
         "mask-size: 12px;" +
@@ -24,10 +24,12 @@ function remoteRender(event) {
         "mask-position: center;" +
         "-webkit-mask-repeat: no-repeat;" +
         "mask-repeat: no-repeat;" +
+        "display: inline-block;") + "" +
+
+        // Always add these styles
         `-webkit-mask-image: url('${data.imgSrc}');` +
         `mask-image: url('${data.imgSrc}');` +
-        `background-color: ${a.style.color};` +
-        "display: inline-block;";
+        `background-color: ${a.style.color};`;
 
     const body = document.body;
     // Don't display scrollbars if the link takes more than one line to display.

From 4ca838d4c76e8e44fd197face06be179c59388ce Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 11 Mar 2021 11:04:39 +0000
Subject: [PATCH 381/389] Properly gate SpaceRoomView behind labs

---
 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 1961779d0e..f3dff6b217 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1874,7 +1874,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             );
         }
 
-        if (this.state.room?.isSpaceRoom()) {
+        if (SettingsStore.getValue("feature_spaces") && this.state.room?.isSpaceRoom()) {
             return <SpaceRoomView
                 space={this.state.room}
                 justCreatedOpts={this.props.justCreatedOpts}

From 6d81634eec5e22c94d44760d73eaed4a4448c3cb Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 11 Mar 2021 11:49:43 +0000
Subject: [PATCH 382/389] Rebuild space previews with new designs

---
 res/css/structures/_SpaceRoomView.scss      | 121 ++++++++++++++++--
 res/css/views/elements/_FormButton.scss     |   6 +
 src/components/structures/SpaceRoomView.tsx | 132 ++++++++++++++------
 src/i18n/strings/en_EN.json                 |  10 +-
 4 files changed, 214 insertions(+), 55 deletions(-)

diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 38310d39a9..60abe36c29 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -20,6 +20,8 @@ $SpaceRoomViewInnerWidth: 428px;
     .mx_MainSplit > div:first-child {
         padding: 80px 60px;
         flex-grow: 1;
+        max-height: 100%;
+        overflow-y: auto;
 
         h1 {
             margin: 0;
@@ -69,9 +71,116 @@ $SpaceRoomViewInnerWidth: 428px;
         }
     }
 
-    .mx_SpaceRoomView_landing {
-        overflow-y: auto;
+    .mx_SpaceRoomView_preview {
+        padding: 32px 24px !important; // override default padding from above
+        margin: auto;
+        max-width: 480px;
+        box-sizing: border-box;
+        box-shadow: 2px 15px 30px $dialog-shadow-color;
+        border: 1px solid $input-border-color;
+        border-radius: 8px;
 
+        .mx_SpaceRoomView_preview_inviter {
+            display: flex;
+            align-items: center;
+            margin-bottom: 20px;
+            font-size: $font-15px;
+
+            > div {
+                margin-left: 8px;
+
+                .mx_SpaceRoomView_preview_inviter_name {
+                    line-height: $font-18px;
+                }
+
+                .mx_SpaceRoomView_preview_inviter_mxid {
+                    line-height: $font-24px;
+                    color: $secondary-fg-color;
+                }
+            }
+        }
+
+        > .mx_BaseAvatar_image,
+        > .mx_BaseAvatar > .mx_BaseAvatar_image {
+            border-radius: 12px;
+        }
+
+        h1.mx_SpaceRoomView_preview_name {
+            margin: 20px 0 !important; // override default margin from above
+        }
+
+        .mx_SpaceRoomView_preview_info {
+            color: $tertiary-fg-color;
+            font-size: $font-15px;
+            line-height: $font-24px;
+            margin: 20px 0;
+
+            .mx_SpaceRoomView_preview_info_public,
+            .mx_SpaceRoomView_preview_info_private {
+                padding-left: 20px;
+                position: relative;
+
+                &::before {
+                    position: absolute;
+                    content: "";
+                    width: 20px;
+                    height: 20px;
+                    top: 0;
+                    left: -2px;
+                    mask-position: center;
+                    mask-repeat: no-repeat;
+                    background-color: $tertiary-fg-color;
+                }
+            }
+
+            .mx_SpaceRoomView_preview_info_public::before {
+                mask-size: 12px;
+                mask-image: url("$(res)/img/globe.svg");
+            }
+
+            .mx_SpaceRoomView_preview_info_private::before {
+                mask-size: 14px;
+                mask-image: url("$(res)/img/element-icons/lock.svg");
+            }
+
+            .mx_AccessibleButton_kind_link {
+                color: inherit;
+                position: relative;
+                padding-left: 16px;
+
+                &::before {
+                    content: "·"; // visual separator
+                    position: absolute;
+                    left: 6px;
+                }
+            }
+        }
+
+        .mx_SpaceRoomView_preview_topic {
+            font-size: $font-14px;
+            line-height: $font-22px;
+            color: $secondary-fg-color;
+            margin: 20px 0;
+            max-height: 160px;
+            overflow-y: auto;
+        }
+
+        .mx_SpaceRoomView_preview_joinButtons {
+            margin-top: 20px;
+
+            .mx_AccessibleButton {
+                width: 200px;
+                box-sizing: border-box;
+                padding: 14px 0;
+
+                & + .mx_AccessibleButton {
+                    margin-left: 20px;
+                }
+            }
+        }
+    }
+
+    .mx_SpaceRoomView_landing {
         > .mx_BaseAvatar_image,
         > .mx_BaseAvatar > .mx_BaseAvatar_image {
             border-radius: 12px;
@@ -128,14 +237,6 @@ $SpaceRoomViewInnerWidth: 428px;
             font-size: $font-15px;
         }
 
-        .mx_SpaceRoomView_landing_joinButtons {
-            margin-top: 24px;
-
-            .mx_FormButton {
-                padding: 8px 22px;
-            }
-        }
-
         .mx_SpaceRoomView_landing_adminButtons {
             margin-top: 32px;
 
diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss
index 7ec01f17e6..eda201ff03 100644
--- a/res/css/views/elements/_FormButton.scss
+++ b/res/css/views/elements/_FormButton.scss
@@ -33,4 +33,10 @@ limitations under the License.
         color: $notice-primary-color;
         background-color: $notice-primary-bg-color;
     }
+
+    &.mx_AccessibleButton_kind_secondary {
+        color: $secondary-fg-color;
+        border: 1px solid $secondary-fg-color;
+        background-color: unset;
+    }
 }
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 9bacdd975d..0b0f2a2ac9 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -94,26 +94,95 @@ const useMyRoomMembership = (room: Room) => {
     return membership;
 };
 
-const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
+const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
     const cli = useContext(MatrixClientContext);
     const myMembership = useMyRoomMembership(space);
-    const joinRule = space.getJoinRule();
-    const userId = cli.getUserId();
 
+    let inviterSection;
     let joinButtons;
     if (myMembership === "invite") {
-        joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
-            <FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} />
-            <AccessibleButton kind="link" onClick={onRejectButtonClicked}>
-                {_t("Decline")}
-            </AccessibleButton>
-        </div>;
-    } else if (myMembership !== "join" && joinRule === "public") {
-        joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons">
-            <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
-        </div>;
+        const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
+        const inviter = inviteSender && space.getMember(inviteSender);
+
+        if (inviteSender) {
+            inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
+                <MemberAvatar member={inviter} width={32} height={32} />
+                <div>
+                    <div className="mx_SpaceRoomView_preview_inviter_name">
+                        { _t("<inviter/> invites you", {}, {
+                            inviter: () => <b>{ inviter.name || inviteSender }</b>,
+                        }) }
+                    </div>
+                    { inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
+                        { inviteSender }
+                    </div> : null }
+                </div>
+            </div>;
+        }
+
+        joinButtons = <>
+            <FormButton label={_t("Reject")} kind="secondary" onClick={onRejectButtonClicked} />
+            <FormButton label={_t("Accept")} onClick={onJoinButtonClicked} />
+        </>;
+    } else {
+        joinButtons = <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
     }
 
+    let visibilitySection;
+    if (space.getJoinRule() === "public") {
+        visibilitySection = <span className="mx_SpaceRoomView_preview_info_public">
+            { _t("Public space") }
+        </span>;
+    } else {
+        visibilitySection = <span className="mx_SpaceRoomView_preview_info_private">
+            { _t("Private space") }
+        </span>;
+    }
+
+    return <div className="mx_SpaceRoomView_preview">
+        { inviterSection }
+        <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
+        <h1 className="mx_SpaceRoomView_preview_name">
+            <RoomName room={space} />
+        </h1>
+        <div className="mx_SpaceRoomView_preview_info">
+            { visibilitySection }
+            <RoomMemberCount room={space}>
+                {(count) => count > 0 ? (
+                    <AccessibleButton
+                        className="mx_SpaceRoomView_preview_memberCount"
+                        kind="link"
+                        onClick={() => {
+                            defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
+                                action: Action.SetRightPanelPhase,
+                                phase: RightPanelPhases.RoomMemberList,
+                                refireParams: { space },
+                            });
+                        }}
+                    >
+                        { _t("%(count)s members", { count }) }
+                    </AccessibleButton>
+                ) : null}
+            </RoomMemberCount>
+        </div>
+        <RoomTopic room={space}>
+            {(topic, ref) =>
+                <div className="mx_SpaceRoomView_preview_topic" ref={ref}>
+                    { topic }
+                </div>
+            }
+        </RoomTopic>
+        <div className="mx_SpaceRoomView_preview_joinButtons">
+            { joinButtons }
+        </div>
+    </div>;
+};
+
+const SpaceLanding = ({ space }) => {
+    const cli = useContext(MatrixClientContext);
+    const myMembership = useMyRoomMembership(space);
+    const userId = cli.getUserId();
+
     let inviteButton;
     if (myMembership === "join" && space.canInvite(userId)) {
         inviteButton = (
@@ -227,26 +296,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
                             ) : null}
                         </RoomMemberCount>
                     </div> };
-                    if (myMembership === "invite") {
-                        const inviteSender = space.getMember(userId)?.events.member?.getSender();
-                        const inviter = inviteSender && space.getMember(inviteSender);
-
-                        if (inviteSender) {
-                            return _t("<inviter/> invited you to <name/>", {}, {
-                                name: tags.name,
-                                inviter: () => inviter
-                                    ? <span className="mx_SpaceRoomView_landing_inviter">
-                                        <MemberAvatar member={inviter} width={26} height={26} viewUserOnClick={true} />
-                                        { inviter.name }
-                                    </span>
-                                    : <span className="mx_SpaceRoomView_landing_inviter">
-                                        { inviteSender }
-                                    </span>,
-                            }) as JSX.Element;
-                        } else {
-                            return _t("You have been invited to <name/>", {}, tags) as JSX.Element;
-                        }
-                    } else if (shouldShowSpaceSettings(cli, space)) {
+                    if (shouldShowSpaceSettings(cli, space)) {
                         if (space.getJoinRule() === "public") {
                             return _t("Your public space <name/>", {}, tags) as JSX.Element;
                         } else {
@@ -260,7 +310,6 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
         <div className="mx_SpaceRoomView_landing_topic">
             <RoomTopic room={space} />
         </div>
-        { joinButtons }
         <div className="mx_SpaceRoomView_landing_adminButtons">
             { inviteButton }
             { addRoomButtons }
@@ -548,12 +597,15 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
     private renderBody() {
         switch (this.state.phase) {
             case Phase.Landing:
-                return <SpaceLanding
-                    space={this.props.space}
-                    onJoinButtonClicked={this.props.onJoinButtonClicked}
-                    onRejectButtonClicked={this.props.onRejectButtonClicked}
-                />;
-
+                if (this.props.space.getMyMembership() === "join") {
+                    return <SpaceLanding space={this.props.space} />;
+                } else {
+                    return <SpacePreview
+                        space={this.props.space}
+                        onJoinButtonClicked={this.props.onJoinButtonClicked}
+                        onRejectButtonClicked={this.props.onRejectButtonClicked}
+                    />;
+                }
             case Phase.PublicCreateRooms:
                 return <SpaceSetupFirstRooms
                     space={this.props.space}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 71aae7fecd..23172d2c13 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2601,14 +2601,14 @@
     "Promoted to users": "Promoted to users",
     "Manage rooms": "Manage rooms",
     "Find a room...": "Find a room...",
-    "Accept Invite": "Accept Invite",
+    "<inviter/> invites you": "<inviter/> invites you",
+    "Public space": "Public space",
+    "Private space": "Private space",
+    "%(count)s members|other": "%(count)s members",
+    "%(count)s members|one": "%(count)s member",
     "Add existing rooms & spaces": "Add existing rooms & spaces",
     "Default Rooms": "Default Rooms",
     "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
-    "%(count)s members|other": "%(count)s members",
-    "%(count)s members|one": "%(count)s member",
-    "<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",
-    "You have been invited to <name/>": "You have been invited to <name/>",
     "Your public space <name/>": "Your public space <name/>",
     "Your private space <name/>": "Your private space <name/>",
     "Welcome to <name/>": "Welcome to <name/>",

From c2e05c647e499e60fbda85f5942db7bb7b689403 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 11 Mar 2021 11:56:53 +0000
Subject: [PATCH 383/389] Remove unused common CSS classes

---
 res/css/_common.scss | 48 --------------------------------------------
 1 file changed, 48 deletions(-)

diff --git a/res/css/_common.scss b/res/css/_common.scss
index 8ae1cc6641..36a81e6651 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -489,54 +489,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
     margin-top: 69px;
 }
 
-.mx_Beta {
-    color: red;
-    margin-right: 10px;
-    position: relative;
-    top: -3px;
-    background-color: white;
-    padding: 0 4px;
-    border-radius: 3px;
-    border: 1px solid darkred;
-    cursor: help;
-    transition-duration: 200ms;
-    font-size: smaller;
-    filter: opacity(0.5);
-}
-
-.mx_Beta:hover {
-    color: white;
-    border: 1px solid gray;
-    background-color: darkred;
-}
-
-.mx_TintableSvgButton {
-    position: relative;
-    display: flex;
-    flex-direction: row;
-    justify-content: center;
-    align-content: center;
-}
-
-.mx_TintableSvgButton object {
-    margin: 0;
-    padding: 0;
-    width: 100%;
-    height: 100%;
-    max-width: 100%;
-    max-height: 100%;
-}
-
-.mx_TintableSvgButton span {
-    position: absolute;
-    top: 0;
-    right: 0;
-    bottom: 0;
-    left: 0;
-    opacity: 0;
-    cursor: pointer;
-}
-
 // username colors
 // used by SenderProfile & RoomPreviewBar
 .mx_Username_color1 {

From b2d4639ec9a5a768415d37f9aff0a255cc55c0ae Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Thu, 11 Mar 2021 13:40:11 +0000
Subject: [PATCH 384/389] Use fsync in reskindex to ensure file is written to
 disk

This should (hopefully) resolve occasional errors where the rename step would
fail because the temporary file did not exist. In addition, this also exits with
an error code if something goes wrong so we notice it early, rather than having
to scroll through pages of logs at release time.
---
 scripts/reskindex.js | 24 +++++++++++++++---------
 1 file changed, 15 insertions(+), 9 deletions(-)

diff --git a/scripts/reskindex.js b/scripts/reskindex.js
index 12310b77c1..5eaec4d1d5 100755
--- a/scripts/reskindex.js
+++ b/scripts/reskindex.js
@@ -1,5 +1,6 @@
 #!/usr/bin/env node
 const fs = require('fs');
+const { promises: fsp } = fs;
 const path = require('path');
 const glob = require('glob');
 const util = require('util');
@@ -25,6 +26,8 @@ async function reskindex() {
     const header = args.h || args.header;
 
     const strm = fs.createWriteStream(componentIndexTmp);
+    // Wait for the open event to ensure the file descriptor is set
+    await new Promise(resolve => strm.once("open", resolve));
 
     if (header) {
        strm.write(fs.readFileSync(header));
@@ -53,14 +56,9 @@ async function reskindex() {
 
     strm.write("export {components};\n");
     // Ensure the file has been fully written to disk before proceeding
+    await util.promisify(fs.fsync)(strm.fd);
     await util.promisify(strm.end);
-    fs.rename(componentIndexTmp, componentIndex, function(err) {
-        if (err) {
-            console.error("Error moving new index into place: " + err);
-        } else {
-            console.log('Reskindex: completed');
-        }
-    });
+    await fsp.rename(componentIndexTmp, componentIndex);
 }
 
 // Expects both arrays of file names to be sorted
@@ -77,9 +75,17 @@ function filesHaveChanged(files, prevFiles) {
     return false;
 }
 
+// Wrapper since await at the top level is not well supported yet
+function run() {
+    (async function() {
+        await reskindex();
+        console.log("Reskindex completed");
+    })();
+}
+
 // -w indicates watch mode where any FS events will trigger reskindex
 if (!args.w) {
-    reskindex();
+    run();
     return;
 }
 
@@ -87,5 +93,5 @@ let watchDebouncer = null;
 chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
     if (path === componentIndex) return;
     if (watchDebouncer) clearTimeout(watchDebouncer);
-    watchDebouncer = setTimeout(reskindex, 1000);
+    watchDebouncer = setTimeout(run, 1000);
 });

From 90817f3bc6302777dca14d9003916743637788a6 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 11 Mar 2021 09:19:34 -0700
Subject: [PATCH 385/389] Developer documentation

The lint rules don't currently exist, but would apply to `mxcUrlToHttp` and `getHttpUriForMxc`
---
 docs/media-handling.md | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 docs/media-handling.md

diff --git a/docs/media-handling.md b/docs/media-handling.md
new file mode 100644
index 0000000000..a4307fb7d4
--- /dev/null
+++ b/docs/media-handling.md
@@ -0,0 +1,19 @@
+# Media handling
+
+Surely media should be as easy as just putting a URL into an `img` and calling it good, right?
+Not quite. Matrix uses something called a Matrix Content URI (better known as MXC URI) to identify
+content, which is then converted to a regular HTTPS URL on the homeserver. However, sometimes that
+URL can change depending on deployment considerations.
+
+The react-sdk features a [customisation endpoint](https://github.com/vector-im/element-web/blob/develop/docs/customisations.md)
+for media handling where all conversions from MXC URI to HTTPS URL happen. This is to ensure that
+those obscure deployments can route all their media to the right place.
+
+For development, there are currently two functions available: `mediaFromMxc` and `mediaFromContent`.
+The `mediaFromMxc` function should be self-explanatory. `mediaFromContent` takes an event content as
+a parameter and will automatically parse out the source media and thumbnail. Both functions return
+a `Media` object with a number of options on it, such as getting various common HTTPS URLs for the
+media.
+
+**It is extremely important that all media calls are put through this customisation endpoint.** So
+much so it's a lint rule to avoid accidental use of the wrong functions.

From 6ab235f10c5bc502182e7a810e0fdbe6e650deee Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Thu, 11 Mar 2021 09:42:55 -0700
Subject: [PATCH 386/389] Fix tests for new call path

We have to mock `fetch` for the caching of the download icon, and then mock out all the function calls used by components to feed a Media object.
---
 package.json                                  |  1 +
 .../structures/MessagePanel-test.js           |  4 +++
 .../elements/MemberEventListSummary-test.js   |  1 +
 .../views/messages/TextualBody-test.js        |  5 ++++
 test/setupTests.js                            |  2 ++
 test/test-utils.js                            |  2 ++
 yarn.lock                                     | 25 +++++++++++++++++++
 7 files changed, 40 insertions(+)

diff --git a/package.json b/package.json
index 7ed1b272da..23af090fd1 100644
--- a/package.json
+++ b/package.json
@@ -157,6 +157,7 @@
     "jest": "^26.6.3",
     "jest-canvas-mock": "^2.3.0",
     "jest-environment-jsdom-sixteen": "^1.0.3",
+    "jest-fetch-mock": "^3.0.3",
     "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",
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 2fd5bd6ad1..7347ff2658 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -116,6 +116,7 @@ describe('MessagePanel', function() {
                     getAvatarUrl: () => {
                         return "avatar.jpeg";
                     },
+                    getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
                 },
                 ts: ts0 + i*1000,
                 mship: 'join',
@@ -148,6 +149,7 @@ describe('MessagePanel', function() {
                     getAvatarUrl: () => {
                         return "avatar.jpeg";
                     },
+                    getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
                 },
                 ts: ts0 + i*1000,
                 mship: 'join',
@@ -193,6 +195,7 @@ describe('MessagePanel', function() {
                     getAvatarUrl: () => {
                         return "avatar.jpeg";
                     },
+                    getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
                 },
                 ts: ts0 + 1,
                 mship: 'join',
@@ -239,6 +242,7 @@ describe('MessagePanel', function() {
                     getAvatarUrl: () => {
                         return "avatar.jpeg";
                     },
+                    getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
                 },
                 ts: ts0 + 5,
                 mship: 'invite',
diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js
index 6d26fa36e9..dd6febc7d7 100644
--- a/test/components/views/elements/MemberEventListSummary-test.js
+++ b/test/components/views/elements/MemberEventListSummary-test.js
@@ -50,6 +50,7 @@ describe('MemberEventListSummary', function() {
                 getAvatarUrl: () => {
                     return "avatar.jpeg";
                 },
+                getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
             },
         });
         // Override random event ID to allow for equality tests against tiles from
diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js
index a596825c09..0a6d47a72b 100644
--- a/test/components/views/messages/TextualBody-test.js
+++ b/test/components/views/messages/TextualBody-test.js
@@ -37,6 +37,7 @@ describe("<TextualBody />", () => {
             getRoom: () => mkStubRoom("room_id"),
             getAccountData: () => undefined,
             isGuest: () => false,
+            mxcUrlToHttp: (s) => s,
         };
 
         const ev = mkEvent({
@@ -61,6 +62,7 @@ describe("<TextualBody />", () => {
             getRoom: () => mkStubRoom("room_id"),
             getAccountData: () => undefined,
             isGuest: () => false,
+            mxcUrlToHttp: (s) => s,
         };
 
         const ev = mkEvent({
@@ -86,6 +88,7 @@ describe("<TextualBody />", () => {
                 getRoom: () => mkStubRoom("room_id"),
                 getAccountData: () => undefined,
                 isGuest: () => false,
+                mxcUrlToHttp: (s) => s,
             };
         });
 
@@ -139,6 +142,7 @@ describe("<TextualBody />", () => {
                 on: () => undefined,
                 removeListener: () => undefined,
                 isGuest: () => false,
+                mxcUrlToHttp: (s) => s,
             };
         });
 
@@ -284,6 +288,7 @@ describe("<TextualBody />", () => {
             getAccountData: () => undefined,
             getUrlPreview: (url) => new Promise(() => {}),
             isGuest: () => false,
+            mxcUrlToHttp: (s) => s,
         };
 
         const ev = mkEvent({
diff --git a/test/setupTests.js b/test/setupTests.js
index 9c2d16a8df..6d37d48987 100644
--- a/test/setupTests.js
+++ b/test/setupTests.js
@@ -2,3 +2,5 @@ import * as languageHandler from "../src/languageHandler";
 
 languageHandler.setLanguage('en');
 languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]);
+
+require('jest-fetch-mock').enableMocks();
diff --git a/test/test-utils.js b/test/test-utils.js
index b6e0468d86..d259fcb95f 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -213,6 +213,7 @@ export function mkStubRoom(roomId = null) {
             rawDisplayName: 'Member',
             roomId: roomId,
             getAvatarUrl: () => 'mxc://avatar.url/image.png',
+            getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
         }),
         getMembersWithMembership: jest.fn().mockReturnValue([]),
         getJoinedMembers: jest.fn().mockReturnValue([]),
@@ -242,6 +243,7 @@ export function mkStubRoom(roomId = null) {
         removeListener: jest.fn(),
         getDMInviter: jest.fn(),
         getAvatarUrl: () => 'mxc://avatar.url/room.png',
+        getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
     };
 }
 
diff --git a/yarn.lock b/yarn.lock
index f99ea5900d..89ad76638f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2589,6 +2589,13 @@ crc-32@^0.3.0:
   resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-0.3.0.tgz#6a3d3687f5baec41f7e9b99fe1953a2e5d19775e"
   integrity sha1-aj02h/W67EH36bmf4ZU6Ll0Zd14=
 
+cross-fetch@^3.0.4:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c"
+  integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==
+  dependencies:
+    node-fetch "2.6.1"
+
 cross-spawn@^6.0.0, cross-spawn@^6.0.5:
   version "6.0.5"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@@ -4918,6 +4925,14 @@ jest-environment-node@^26.6.2:
     jest-mock "^26.6.2"
     jest-util "^26.6.2"
 
+jest-fetch-mock@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
+  integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
+  dependencies:
+    cross-fetch "^3.0.4"
+    promise-polyfill "^8.1.3"
+
 jest-get-type@^26.3.0:
   version "26.3.0"
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
@@ -5835,6 +5850,11 @@ nice-try@^1.0.4:
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
+node-fetch@2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
+  integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+
 node-fetch@^1.0.1:
   version "1.7.3"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@@ -6448,6 +6468,11 @@ progress@^2.0.0:
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
+promise-polyfill@^8.1.3:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0"
+  integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==
+
 promise@^7.0.3, promise@^7.1.1:
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"

From a1eabde3a36b11fb3da2469b04f0b1a5b3d6dbe9 Mon Sep 17 00:00:00 2001
From: Panagiotis <27917356+panoschal@users.noreply.github.com>
Date: Thu, 11 Mar 2021 19:14:04 +0200
Subject: [PATCH 387/389] fix: create a PillCandidatePart on the beginning of a
 part string

---
 src/editor/parts.ts | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/editor/parts.ts b/src/editor/parts.ts
index 67f6a2c0c5..ccd90da3e2 100644
--- a/src/editor/parts.ts
+++ b/src/editor/parts.ts
@@ -189,7 +189,13 @@ abstract class PlainBasePart extends BasePart {
             if (chr !== "@" && chr !== "#" && chr !== ":" && chr !== "+") {
                 return true;
             }
-            // only split if the previous character is a space
+
+            // split if we are at the beginning of the part text
+            if (offset === 0) {
+                return false;
+            }
+
+            // or split if the previous character is a space
             // or if it is a + and this is a :
             return this._text[offset - 1] !== " " &&
                 (this._text[offset - 1] !== "+" || chr !== ":");

From 62fa60f1093faad3c5374ef3f881dcd5781f085b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 11 Mar 2021 08:29:03 +0100
Subject: [PATCH 388/389] Use resourceId in formatted body
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/editor/serialize.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index c1f4da306b..40ff4b59f5 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -34,6 +34,10 @@ export function mdSerialize(model: EditorModel) {
             case "at-room-pill":
                 return html + part.text;
             case "room-pill":
+                // Here we use the resourceId for compatibility with non-rich text clients
+                // See https://github.com/vector-im/element-web/issues/16660
+                return html +
+                    `[${part.resourceId.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
             case "user-pill":
                 return html +
                     `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;

From d4f67d94a4ec1d2115d9b6fc150db698c9d9ceae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 11 Mar 2021 18:50:35 +0100
Subject: [PATCH 389/389] Use resourceId in plain body
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
---
 src/editor/serialize.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 40ff4b59f5..d43cb4f38d 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -101,6 +101,9 @@ export function textSerialize(model: EditorModel) {
             case "at-room-pill":
                 return text + part.text;
             case "room-pill":
+                // Here we use the resourceId for compatibility with non-rich text clients
+                // See https://github.com/vector-im/element-web/issues/16660
+                return text + `${part.resourceId}`;
             case "user-pill":
                 return text + `${part.text}`;
         }