From 61a260cd405eacac4c52777bd048657a9fda3702 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Sun, 18 Apr 2021 15:40:56 +0200
Subject: [PATCH 001/159] Format mxid

---
 res/css/views/messages/_SenderProfile.scss    | 12 ++++++
 .../views/messages/SenderProfile.js           | 43 ++++++++++++++-----
 2 files changed, 45 insertions(+), 10 deletions(-)

diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss
index 655cb39489..e3a38bad6d 100644
--- a/res/css/views/messages/_SenderProfile.scss
+++ b/res/css/views/messages/_SenderProfile.scss
@@ -15,6 +15,18 @@ limitations under the License.
 */
 
 .mx_SenderProfile_name {
+    display: flex;
+    align-items: center;
+    gap: 5px;
+}
+
+.mx_SenderProfile_displayName {
     font-weight: 600;
 }
 
+.mx_SenderProfile_mxid {
+    font-weight: 600;
+    font-family: monospace;
+    font-size: 1rem;
+    color: gray;
+}
diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js
index bd10526799..264d4ed3de 100644
--- a/src/components/views/messages/SenderProfile.js
+++ b/src/components/views/messages/SenderProfile.js
@@ -89,7 +89,21 @@ export default class SenderProfile extends React.Component {
     render() {
         const {mxEvent} = this.props;
         const colorClass = getUserNameColorClass(mxEvent.getSender());
-        const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
+
+        let disambiguate;
+        let displayName;
+        let mxid;
+
+        const sender = mxEvent.sender;
+        if (sender) {
+            disambiguate = sender.disambiguate;
+            displayName = sender.rawDisplayName;
+            mxid = sender.userId;
+        } else {
+            disambiguate = false;
+            displayName = mxEvent.getSender();
+            mxid = mxEvent.getSender();
+        }
         const {msgtype} = mxEvent.getContent();
 
         if (msgtype === 'm.emote') {
@@ -108,20 +122,29 @@ export default class SenderProfile extends React.Component {
             />;
         }
 
-        const nameElem = name || '';
-
-        // Name + flair
-        const nameFlair = <span>
-            <span className={`mx_SenderProfile_name ${colorClass}`}>
-                { nameElem }
+        const displayNameElement = (
+            <span className={`mx_SenderProfile_displayName ${colorClass}`}>
+                { displayName || '' }
             </span>
-            { flair }
-        </span>;
+        );
+
+        let mxidElement;
+        if (disambiguate) {
+            mxidElement = (
+                <span className="mx_SenderProfile_mxid">
+                    { `[${mxid || ""}]` }
+                </span>
+            );
+        }
 
         return (
             <div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
                 <div className="mx_SenderProfile_hover">
-                    { nameFlair }
+                    <div className="mx_SenderProfile_name">
+                        { displayNameElement }
+                        { mxidElement }
+                        { flair }
+                    </div>
                 </div>
             </div>
         );

From 39eb487f49718453ac152d97119f764527a67821 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 20 Apr 2021 11:09:03 +0200
Subject: [PATCH 002/159] TS conversion
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>
---
 .../{SenderProfile.js => SenderProfile.tsx}   | 58 +++++++++----------
 1 file changed, 29 insertions(+), 29 deletions(-)
 rename src/components/views/messages/{SenderProfile.js => SenderProfile.tsx} (82%)

diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.tsx
similarity index 82%
rename from src/components/views/messages/SenderProfile.js
rename to src/components/views/messages/SenderProfile.tsx
index 264d4ed3de..fa635eaedb 100644
--- a/src/components/views/messages/SenderProfile.js
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -15,26 +15,37 @@
  */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 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";
+import MatrixEvent from "matrix-js-sdk/src/models/event";
+
+interface IProps {
+    mxEvent: MatrixEvent;
+    onClick(): void;
+    enableFlair: boolean;
+}
+
+interface IState {
+    userGroups;
+    relatedGroups;
+}
 
 @replaceableComponent("views.messages.SenderProfile")
-export default class SenderProfile extends React.Component {
-    static propTypes = {
-        mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
-        onClick: PropTypes.func,
-    };
-
+export default class SenderProfile extends React.Component<IProps, IState> {
     static contextType = MatrixClientContext;
+    unmounted: boolean;
 
-    state = {
-        userGroups: null,
-        relatedGroups: [],
-    };
+    constructor(props: IProps) {
+        super(props)
+
+        this.state = {
+            userGroups: null,
+            relatedGroups: [],
+        };
+    }
 
     componentDidMount() {
         this.unmounted = false;
@@ -89,28 +100,17 @@ export default class SenderProfile extends React.Component {
     render() {
         const {mxEvent} = this.props;
         const colorClass = getUserNameColorClass(mxEvent.getSender());
-
-        let disambiguate;
-        let displayName;
-        let mxid;
-
-        const sender = mxEvent.sender;
-        if (sender) {
-            disambiguate = sender.disambiguate;
-            displayName = sender.rawDisplayName;
-            mxid = sender.userId;
-        } else {
-            disambiguate = false;
-            displayName = mxEvent.getSender();
-            mxid = mxEvent.getSender();
-        }
         const {msgtype} = mxEvent.getContent();
 
+        const disambiguate = mxEvent.sender?.disambiguate;
+        const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || "";
+        const mxid = mxEvent.sender?.userId || mxEvent.getSender() || "";
+
         if (msgtype === 'm.emote') {
             return <span />; // emote message must include the name so don't duplicate it
         }
 
-        let flair = <div />;
+        let flair;
         if (this.props.enableFlair) {
             const displayedGroups = this._getDisplayedGroups(
                 this.state.userGroups, this.state.relatedGroups,
@@ -124,7 +124,7 @@ export default class SenderProfile extends React.Component {
 
         const displayNameElement = (
             <span className={`mx_SenderProfile_displayName ${colorClass}`}>
-                { displayName || '' }
+                { displayName }
             </span>
         );
 
@@ -132,7 +132,7 @@ export default class SenderProfile extends React.Component {
         if (disambiguate) {
             mxidElement = (
                 <span className="mx_SenderProfile_mxid">
-                    { `[${mxid || ""}]` }
+                    { `[${mxid}]` }
                 </span>
             );
         }

From faca498523460e59215bbe819521fcda01069729 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 20 Apr 2021 11:12:16 +0200
Subject: [PATCH 003/159] Don't use gap
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/messages/_SenderProfile.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss
index e3a38bad6d..d7763cda08 100644
--- a/res/css/views/messages/_SenderProfile.scss
+++ b/res/css/views/messages/_SenderProfile.scss
@@ -17,7 +17,6 @@ limitations under the License.
 .mx_SenderProfile_name {
     display: flex;
     align-items: center;
-    gap: 5px;
 }
 
 .mx_SenderProfile_displayName {
@@ -29,4 +28,5 @@ limitations under the License.
     font-family: monospace;
     font-size: 1rem;
     color: gray;
+    margin-left: 5px;
 }

From 4e1409dc2c7258816e51efb204a09ef608055117 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 20 Apr 2021 11:40:50 +0200
Subject: [PATCH 004/159] Add private

Co-authored-by: Michael Telatynski <7t3chguy@googlemail.com>
---
 src/components/views/messages/SenderProfile.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx
index fa635eaedb..f0dd77f746 100644
--- a/src/components/views/messages/SenderProfile.tsx
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -36,7 +36,7 @@ interface IState {
 @replaceableComponent("views.messages.SenderProfile")
 export default class SenderProfile extends React.Component<IProps, IState> {
     static contextType = MatrixClientContext;
-    unmounted: boolean;
+    private unmounted: boolean;
 
     constructor(props: IProps) {
         super(props)

From ffcd79f4a3215c1153e50ec599a8cd18b2d1e5a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 21 Apr 2021 17:34:03 +0200
Subject: [PATCH 005/159] Remove brackets
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/SenderProfile.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx
index f0dd77f746..b2bfe00168 100644
--- a/src/components/views/messages/SenderProfile.tsx
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -132,7 +132,7 @@ export default class SenderProfile extends React.Component<IProps, IState> {
         if (disambiguate) {
             mxidElement = (
                 <span className="mx_SenderProfile_mxid">
-                    { `[${mxid}]` }
+                    { mxid }
                 </span>
             );
         }

From eee1294374c568ee3e9c956b5fb3acaf537918a2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 21 Apr 2021 17:37:25 +0200
Subject: [PATCH 006/159] Make both have the same baseline
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/messages/_SenderProfile.scss      | 5 -----
 src/components/views/messages/SenderProfile.tsx | 8 +++-----
 2 files changed, 3 insertions(+), 10 deletions(-)

diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss
index d7763cda08..14e8f6583f 100644
--- a/res/css/views/messages/_SenderProfile.scss
+++ b/res/css/views/messages/_SenderProfile.scss
@@ -14,11 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-.mx_SenderProfile_name {
-    display: flex;
-    align-items: center;
-}
-
 .mx_SenderProfile_displayName {
     font-weight: 600;
 }
diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx
index b2bfe00168..70040e4ef7 100644
--- a/src/components/views/messages/SenderProfile.tsx
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -140,11 +140,9 @@ export default class SenderProfile extends React.Component<IProps, IState> {
         return (
             <div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
                 <div className="mx_SenderProfile_hover">
-                    <div className="mx_SenderProfile_name">
-                        { displayNameElement }
-                        { mxidElement }
-                        { flair }
-                    </div>
+                    { displayNameElement }
+                    { mxidElement }
+                    { flair }
                 </div>
             </div>
         );

From 9658a760c8bd96dd91498d064e7898c5a70fe19f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 21 Apr 2021 17:41:21 +0200
Subject: [PATCH 007/159] Use timestamp color
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/messages/_SenderProfile.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss
index 14e8f6583f..378d9e6f1a 100644
--- a/res/css/views/messages/_SenderProfile.scss
+++ b/res/css/views/messages/_SenderProfile.scss
@@ -22,6 +22,6 @@ limitations under the License.
     font-weight: 600;
     font-family: monospace;
     font-size: 1rem;
-    color: gray;
+    color: $event-timestamp-color;
     margin-left: 5px;
 }

From 55365e632b524015f3dca528e18d2c593861bf0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 22 Apr 2021 07:52:39 +0200
Subject: [PATCH 008/159] Use the correct selector in E2EE tests
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>
---
 test/end-to-end-tests/src/usecases/timeline.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/end-to-end-tests/src/usecases/timeline.js b/test/end-to-end-tests/src/usecases/timeline.js
index 3889dce108..01dc618571 100644
--- a/test/end-to-end-tests/src/usecases/timeline.js
+++ b/test/end-to-end-tests/src/usecases/timeline.js
@@ -122,7 +122,7 @@ function getAllEventTiles(session) {
 }
 
 async function getMessageFromEventTile(eventTile) {
-    const senderElement = await eventTile.$(".mx_SenderProfile_name");
+    const senderElement = await eventTile.$(".mx_SenderProfile_displayName");
     const className = await (await eventTile.getProperty("className")).jsonValue();
     const classNames = className.split(" ");
     const bodyElement = await eventTile.$(".mx_EventTile_body");

From fa534e4755df403ee23f82ecd420b94c8c184d79 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 27 Apr 2021 14:47:56 +0100
Subject: [PATCH 009/159] Add room intro warning when e2ee is not enabled

---
 res/css/views/rooms/_NewRoomIntro.scss      | 20 ++++++++++++++++++++
 src/components/views/rooms/NewRoomIntro.tsx | 20 ++++++++++++++++++++
 src/i18n/strings/en_EN.json                 |  1 +
 3 files changed, 41 insertions(+)

diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss
index 9c2a428cb3..e9d80c48c9 100644
--- a/res/css/views/rooms/_NewRoomIntro.scss
+++ b/res/css/views/rooms/_NewRoomIntro.scss
@@ -69,4 +69,24 @@ limitations under the License.
         font-size: $font-15px;
         color: $secondary-fg-color;
     }
+
+    .mx_NewRoomIntro_message:not(:first-child) {
+        padding-top: 1em;
+        margin-top: 1em;
+        border-top: 1px solid currentColor;
+    }
+
+    .mx_NewRoomIntro_message[role=alert]::before {
+        --size: 25px;
+        content: "!";
+        float: left;
+        border-radius: 50%;
+        width: var(--size);
+        height: var(--size);
+        line-height: var(--size);
+        text-align: center;
+        background: $button-danger-bg-color;
+        color: #fff;
+        margin-right: calc(var(--size) / 2);
+    }
 }
diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 3f6054304d..9b003ade89 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -31,6 +31,14 @@ import dis from "../../../dispatcher/dispatcher";
 import SpaceStore from "../../../stores/SpaceStore";
 import {showSpaceInvite} from "../../../utils/space";
 
+import { privateShouldBeEncrypted } from "../../../createRoom";
+
+function hasExpectedEncryptionSettings(room): boolean {
+    const isEncrypted: boolean = room._client.isRoomEncrypted(room.roomId);
+    const isPublic: boolean = room.getJoinRule() === "public";
+    return isPublic || !privateShouldBeEncrypted() || isEncrypted;
+}
+
 const NewRoomIntro = () => {
     const cli = useContext(MatrixClientContext);
     const {room, roomId} = useContext(RoomContext);
@@ -168,6 +176,18 @@ const NewRoomIntro = () => {
 
     return <div className="mx_NewRoomIntro">
         { body }
+
+        { !hasExpectedEncryptionSettings(room) && <p className="mx_NewRoomIntro_message" role="alert">
+            {_t("Messages in this room are not end-to-end encrypted. " +
+                    "Messages sent in an unencrypted room can be seen by the server and third parties. " +
+                    "<a>Learn more about encryption.</a>", {},
+            {
+                a: sub => <a href="https://element.io/help#encryption"
+                    rel="noreferrer noopener" target="_blank"
+                >{sub}</a>,
+            })}
+
+        </p>}
     </div>;
 };
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index c6f7a8d25e..7ea9227938 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1490,6 +1490,7 @@
     "Invite to just this room": "Invite to just this room",
     "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
     "This is the start of <roomName/>.": "This is the start of <roomName/>.",
+    "Messages in this room are not end-to-end encrypted. Messages sent in an unencrypted room can be seen by the server and third parties. <a>Learn more about encryption.</a>": "Messages in this room are not end-to-end encrypted. Messages sent in an unencrypted room can be seen by the server and third parties. <a>Learn more about encryption.</a>",
     "No pinned messages.": "No pinned messages.",
     "Loading...": "Loading...",
     "Pinned Messages": "Pinned Messages",

From b9b237fc9a531eb3d42d1203198bcb03848c86c9 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sat, 8 May 2021 19:48:34 -0400
Subject: [PATCH 010/159] Replace forwarding UI with dialog

Replaces the old forwarding UI with a dialog based on designs from
https://github.com/vector-im/element-web/issues/14690.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/_components.scss                      |   1 +
 res/css/views/dialogs/_ForwardDialog.scss     | 115 ++++++++++
 .../views/context_menus/MessageContextMenu.js |   8 +-
 .../views/dialogs/ForwardDialog.tsx           | 213 ++++++++++++++++++
 src/i18n/strings/en_EN.json                   |   5 +
 5 files changed, 338 insertions(+), 4 deletions(-)
 create mode 100644 res/css/views/dialogs/_ForwardDialog.scss
 create mode 100644 src/components/views/dialogs/ForwardDialog.tsx

diff --git a/res/css/_components.scss b/res/css/_components.scss
index ec9592f3a1..ebb025d880 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -74,6 +74,7 @@
 @import "./views/dialogs/_DevtoolsDialog.scss";
 @import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
 @import "./views/dialogs/_FeedbackDialog.scss";
+@import "./views/dialogs/_ForwardDialog.scss";
 @import "./views/dialogs/_GroupAddressPicker.scss";
 @import "./views/dialogs/_HostSignupDialog.scss";
 @import "./views/dialogs/_IncomingSasDialog.scss";
diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
new file mode 100644
index 0000000000..a3aa08d04f
--- /dev/null
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -0,0 +1,115 @@
+/*
+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_ForwardDialog {
+    width: 520px;
+    color: $primary-fg-color;
+    display: flex;
+    flex-direction: column;
+    flex-wrap: nowrap;
+    min-height: 0;
+    height: 80vh;
+
+    .mx_ForwardDialog_preview {
+        max-height: 30%;
+        flex-shrink: 0;
+        overflow: scroll;
+
+        div {
+            pointer-events: none;
+        }
+
+        .mx_EventTile_msgOption {
+            display: none;
+        }
+    }
+
+    .mx_ForwardList {
+        display: contents;
+
+        .mx_SearchBox {
+            // To match the space around the title
+            margin: 0 0 15px 0;
+            flex-grow: 0;
+        }
+
+        .mx_ForwardList_content {
+            flex-grow: 1;
+        }
+
+        .mx_ForwardList_noResults {
+            display: block;
+            margin-top: 24px;
+        }
+
+        .mx_ForwardList_section {
+            &:not(:first-child) {
+                margin-top: 24px;
+            }
+
+            > h3 {
+                margin: 0;
+                color: $secondary-fg-color;
+                font-size: $font-12px;
+                font-weight: $font-semi-bold;
+                line-height: $font-15px;
+            }
+
+            .mx_ForwardList_entry {
+                display: flex;
+                margin-top: 12px;
+
+                .mx_BaseAvatar {
+                    margin-right: 12px;
+                }
+
+                .mx_ForwardList_entry_name {
+                    font-size: $font-15px;
+                    line-height: 30px;
+                    flex-grow: 1;
+                    overflow: hidden;
+                    white-space: nowrap;
+                    text-overflow: ellipsis;
+                    margin-right: 12px;
+                }
+
+                .mx_AccessibleButton {
+                    &.mx_ForwardList_sending, &.mx_ForwardList_sent {
+                        &::before {
+                            content: '';
+                            display: inline-block;
+                            background-color: $button-primary-bg-color;
+                            mask-position: center;
+                            mask-repeat: no-repeat;
+                            mask-size: 14px;
+                            width: 14px;
+                            height: 14px;
+                            margin-right: 5px;
+                        }
+                    }
+
+                    &.mx_ForwardList_sending::before {
+                        mask-image: url('$(res)/img/element-icons/circle-sending.svg');
+                    }
+
+                    &.mx_ForwardList_sent::before {
+                        mask-image: url('$(res)/img/element-icons/circle-sent.svg');
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index 35efd12c9c..e069bba975 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -154,11 +154,11 @@ export default class MessageContextMenu extends React.Component {
     };
 
     onForwardClick = () => {
-        if (this.props.onCloseDialog) this.props.onCloseDialog();
-        dis.dispatch({
-            action: 'forward_event',
+        const ForwardDialog = sdk.getComponent("dialogs.ForwardDialog");
+        Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
+            cli: MatrixClientPeg.get(),
             event: this.props.mxEvent,
-        });
+        }, 'mx_Dialog_forwardmessage');
         this.closeMenu();
     };
 
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
new file mode 100644
index 0000000000..ce38a783b3
--- /dev/null
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -0,0 +1,213 @@
+/*
+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, useState, useEffect} from "react";
+import classnames from "classnames";
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import {Room} from "matrix-js-sdk/src/models/room";
+import {MatrixClient} from "matrix-js-sdk/src/client";
+
+import {_t} from "../../../languageHandler";
+import SettingsStore from "../../../settings/SettingsStore";
+import {UIFeature} from "../../../settings/UIFeature";
+import {Layout} from "../../../settings/Layout";
+import {IDialogProps} from "./IDialogProps";
+import BaseDialog from "./BaseDialog";
+import {avatarUrlForUser} from "../../../Avatar";
+import EventTile from "../rooms/EventTile";
+import SearchBox from "../../structures/SearchBox";
+import RoomAvatar from "../avatars/RoomAvatar";
+import AccessibleButton from "../elements/AccessibleButton";
+import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+import DMRoomMap from "../../../utils/DMRoomMap";
+import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
+
+const AVATAR_SIZE = 30;
+
+interface IProps extends IDialogProps {
+    cli: MatrixClient;
+    event: MatrixEvent;
+}
+
+interface IEntryProps {
+    room: Room;
+    // Callback to forward the message to this room
+    send(): Promise<void>;
+}
+
+enum SendState {
+    CanSend,
+    Sending,
+    Sent,
+    Failed,
+}
+
+const Entry: React.FC<IEntryProps> = ({ room, send }) => {
+    const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
+
+    let label;
+    let className;
+    if (sendState === SendState.CanSend) {
+        label = _t("Send");
+        className = "mx_ForwardList_canSend";
+    } else if (sendState === SendState.Sending) {
+        label = _t("Sending…");
+        className = "mx_ForwardList_sending";
+    } else if (sendState === SendState.Sent) {
+        label = _t("Sent");
+        className = "mx_ForwardList_sent";
+    } else {
+        label = _t("Failed to send");
+        className = "mx_ForwardList_sendFailed";
+    }
+
+    return <div className="mx_ForwardList_entry">
+        <RoomAvatar room={room} height={32} width={32} />
+        <span className="mx_ForwardList_entry_name">{ room.name }</span>
+        <AccessibleButton
+            kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
+            className={className}
+            onClick={async () => {
+                setSendState(SendState.Sending);
+                try {
+                    await send();
+                    setSendState(SendState.Sent);
+                } catch (e) {
+                    setSendState(SendState.Failed);
+                }
+            }}
+            disabled={sendState !== SendState.CanSend}
+        >
+            { label }
+        </AccessibleButton>
+    </div>;
+};
+
+const ForwardDialog: React.FC<IProps> = ({ cli, event, onFinished }) => {
+    const userId = cli.getUserId();
+    const [profileInfo, setProfileInfo] = useState<any>({});
+    useEffect(() => {
+        cli.getProfileInfo(userId).then(info => setProfileInfo(info));
+    }, [cli, userId]);
+
+    // For the message preview we fake the sender as ourselves
+    const mockEvent = new MatrixEvent({
+        type: "m.room.message",
+        sender: userId,
+        content: event.getContent(),
+        unsigned: {
+            age: 97,
+        },
+        event_id: "$9999999999999999999999999999999999999999999",
+        room_id: "!999999999999999999:example.org",
+    });
+    mockEvent.sender = {
+        name: profileInfo.displayname || userId,
+        userId: userId,
+        getAvatarUrl: (..._) => {
+            return avatarUrlForUser(
+                { avatarUrl: profileInfo.avatar_url },
+                AVATAR_SIZE, AVATAR_SIZE, "crop",
+            );
+        },
+        getMxcAvatarUrl: () => profileInfo.avatar_url,
+    };
+
+    const visibleRooms = useMemo(() => sortRooms(
+        cli.getVisibleRooms().filter(
+            room => room.getMyMembership() === "join" &&
+                !(SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()),
+        ),
+    ), [cli]);
+
+    const [query, setQuery] = useState("");
+    const lcQuery = query.toLowerCase();
+
+    const [rooms, dms] = visibleRooms.reduce((arr, room) => {
+        if (room.name.toLowerCase().includes(lcQuery)) {
+            if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
+                arr[1].push(room);
+            } else {
+                arr[0].push(room);
+            }
+        }
+        return arr;
+    }, [[], []]);
+
+    const previewLayout = SettingsStore.getValue("layout");
+
+    return <BaseDialog
+        title={ _t("Forward message") }
+        className="mx_ForwardDialog"
+        contentId="mx_ForwardList"
+        onFinished={onFinished}
+        fixedWidth={false}
+    >
+        <div className={classnames("mx_ForwardDialog_preview", {
+            "mx_IRCLayout": previewLayout == Layout.IRC,
+            "mx_GroupLayout": previewLayout == Layout.Group,
+        })}>
+            <EventTile
+                mxEvent={mockEvent}
+                layout={previewLayout}
+                enableFlair={SettingsStore.getValue(UIFeature.Flair)}
+            />
+        </div>
+        <div className="mx_ForwardList">
+            <h2>{ _t("Forward to") }</h2>
+            <SearchBox
+                className="mx_textinput_icon mx_textinput_search"
+                placeholder={ _t("Filter your rooms and DMs") }
+                onSearch={setQuery}
+                autoComplete={true}
+                autoFocus={true}
+            />
+            <AutoHideScrollbar className="mx_ForwardList_content" id="mx_ForwardList">
+                { rooms.length > 0 ? (
+                    <div className="mx_ForwardList_section">
+                        <h3>{ _t("Rooms") }</h3>
+                        { rooms.map(room => {
+                            return <Entry
+                                key={room.roomId}
+                                room={room}
+                                send={() => cli.sendEvent(room.roomId, event.getType(), event.getContent())}
+                            />;
+                        }) }
+                    </div>
+                ) : undefined }
+
+                { dms.length > 0 ? (
+                    <div className="mx_ForwardList_section">
+                        <h3>{ _t("Direct Messages") }</h3>
+                        { dms.map(room => {
+                            return <Entry
+                                key={room.roomId}
+                                room={room}
+                                send={() => cli.sendEvent(room.roomId, event.getType(), event.getContent())}
+                            />;
+                        }) }
+                    </div>
+                ) : null }
+
+                { rooms.length + dms.length < 1 ? <span className="mx_ForwardList_noResults">
+                    { _t("No results") }
+                </span> : undefined }
+            </AutoHideScrollbar>
+        </div>
+    </BaseDialog>;
+};
+
+export default ForwardDialog;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index dcad970300..ed49fb6b44 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2187,6 +2187,11 @@
     "Report a bug": "Report a bug",
     "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
     "Send feedback": "Send feedback",
+    "Sending…": "Sending…",
+    "Sent": "Sent",
+    "Forward message": "Forward message",
+    "Forward to": "Forward to",
+    "Filter your rooms and DMs": "Filter your rooms and DMs",
     "Confirm abort of host creation": "Confirm abort of host creation",
     "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
     "Abort": "Abort",

From 74925b2c6d1f7de459bb6f35bb8cf472f476ccc3 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sat, 8 May 2021 19:51:51 -0400
Subject: [PATCH 011/159] Test ForwardDialog

Signed-off-by: Robin Townsend <robin@robin.town>
---
 .../views/dialogs/ForwardDialog-test.js       | 124 ++++++++++++++++++
 test/test-utils.js                            |   3 +-
 2 files changed, 126 insertions(+), 1 deletion(-)
 create mode 100644 test/components/views/dialogs/ForwardDialog-test.js

diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
new file mode 100644
index 0000000000..cabcbb0325
--- /dev/null
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -0,0 +1,124 @@
+/*
+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 "../../../skinned-sdk";
+
+import React from "react";
+import {configure, mount} from "enzyme";
+import Adapter from "enzyme-adapter-react-16";
+import {act} from "react-dom/test-utils";
+
+import * as TestUtils from "../../../test-utils";
+import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
+import DMRoomMap from "../../../../src/utils/DMRoomMap";
+import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog";
+
+configure({ adapter: new Adapter() });
+
+describe("ForwardDialog", () => {
+    let client;
+    let wrapper;
+
+    const message = TestUtils.mkMessage({
+        room: "!111111111111111111:example.org",
+        user: "@alice:example.org",
+        msg: "Hello world!",
+        event: true,
+    });
+
+    beforeEach(async () => {
+        TestUtils.stubClient();
+        DMRoomMap.makeShared();
+        client = MatrixClientPeg.get();
+        client.getVisibleRooms = jest.fn().mockReturnValue(
+            ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name)),
+        );
+        client.getUserId = jest.fn().mockReturnValue("@bob:example.org");
+
+        await act(async () => {
+            wrapper = mount(
+                <ForwardDialog
+                    cli={client}
+                    event={message}
+                    onFinished={jest.fn()}
+                />,
+            );
+            // Wait one tick for our profile data to load so the state update happens within act
+            await new Promise(resolve => setImmediate(resolve));
+        });
+    });
+
+    it("shows a preview with us as the sender", () => {
+        const previewBody = wrapper.find(".mx_EventTile_body");
+        expect(previewBody.text()).toBe("Hello world!");
+
+        // We would just test SenderProfile for the user ID, but it's stubbed
+        const previewAvatar = wrapper.find(".mx_EventTile_avatar .mx_BaseAvatar_image");
+        expect(previewAvatar.prop("title")).toBe("@bob:example.org");
+    });
+
+    it("filters the rooms", () => {
+        const roomsBefore = wrapper.find(".mx_ForwardList_entry");
+        expect(roomsBefore).toHaveLength(3);
+
+        const searchInput = wrapper.find(".mx_SearchBox input");
+        searchInput.instance().value = "a";
+        searchInput.simulate("change");
+
+        const roomsAfter = wrapper.find(".mx_ForwardList_entry");
+        expect(roomsAfter).toHaveLength(2);
+    });
+
+    it("tracks message sending progress across multiple rooms", async () => {
+        // Make sendEvent require manual resolution so we can see the sending state
+        let finishSend;
+        let cancelSend;
+        client.sendEvent = jest.fn(() => new Promise((resolve, reject) => {
+            finishSend = resolve;
+            cancelSend = reject;
+        }));
+
+        const firstRoom = wrapper.find(".mx_ForwardList_entry").first();
+        expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Send");
+
+        act(() => {
+            firstRoom.find(".mx_AccessibleButton").simulate("click");
+        });
+        expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Sending…");
+
+        await act(async () => {
+            cancelSend();
+            // Wait one tick for the button to realize the send failed
+            await new Promise(resolve => setImmediate(resolve));
+        });
+        expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Failed to send");
+
+        const secondRoom = wrapper.find(".mx_ForwardList_entry").at(1);
+        expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Send");
+
+        act(() => {
+            secondRoom.find(".mx_AccessibleButton").simulate("click");
+        });
+        expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sending…");
+
+        await act(async () => {
+            finishSend();
+            // Wait one tick for the button to realize the send succeeded
+            await new Promise(resolve => setImmediate(resolve));
+        });
+        expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sent");
+    });
+});
diff --git a/test/test-utils.js b/test/test-utils.js
index 6dc02463a5..985e9f804b 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -218,7 +218,7 @@ export function mkMessage(opts) {
     return mkEvent(opts);
 }
 
-export function mkStubRoom(roomId = null) {
+export function mkStubRoom(roomId = null, name) {
     const stubTimeline = { getEvents: () => [] };
     return {
         roomId,
@@ -254,6 +254,7 @@ export function mkStubRoom(roomId = null) {
         on: jest.fn(),
         removeListener: jest.fn(),
         getDMInviter: jest.fn(),
+        name,
         getAvatarUrl: () => 'mxc://avatar.url/room.png',
         getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
         isSpaceRoom: jest.fn(() => false),

From 7fa81766db34449534d265cf7c2e60069049dd5b Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sat, 8 May 2021 20:07:51 -0400
Subject: [PATCH 012/159] Remove old forwarding code

This has been replaced by ForwardDialog.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/structures/RoomView.tsx        | 30 +----------
 src/components/views/rooms/ForwardMessage.js  | 53 -------------------
 src/components/views/rooms/RoomHeader.js      |  9 ----
 .../views/rooms/SimpleRoomHeader.js           | 20 -------
 src/i18n/strings/en_EN.json                   |  1 -
 src/stores/RoomViewStore.tsx                  | 21 --------
 6 files changed, 2 insertions(+), 132 deletions(-)
 delete mode 100644 src/components/views/rooms/ForwardMessage.js

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 58a87e6641..d16247e4ec 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -60,7 +60,6 @@ import ScrollPanel from "./ScrollPanel";
 import TimelinePanel from "./TimelinePanel";
 import ErrorBoundary from "../views/elements/ErrorBoundary";
 import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
-import ForwardMessage from "../views/rooms/ForwardMessage";
 import SearchBar from "../views/rooms/SearchBar";
 import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
 import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
@@ -136,7 +135,6 @@ export interface IState {
     // Whether to highlight the event scrolled to
     isInitialEventHighlighted?: boolean;
     replyToEvent?: MatrixEvent;
-    forwardingEvent?: MatrixEvent;
     numUnreadMessages: number;
     draggingFile: boolean;
     searching: boolean;
@@ -324,7 +322,6 @@ export default class RoomView extends React.Component<IProps, IState> {
             initialEventId: RoomViewStore.getInitialEventId(),
             isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
             replyToEvent: RoomViewStore.getQuotingEvent(),
-            forwardingEvent: RoomViewStore.getForwardingEvent(),
             // we should only peek once we have a ready client
             shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
             showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
@@ -1394,18 +1391,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         dis.dispatch({ action: "open_room_settings" });
     };
 
-    private onCancelClick = () => {
-        console.log("updateTint from onCancelClick");
-        this.updateTint();
-        if (this.state.forwardingEvent) {
-            dis.dispatch({
-                action: 'forward_event',
-                event: null,
-            });
-        }
-        dis.fire(Action.FocusComposer);
-    };
-
     private onAppsClick = () => {
         dis.dispatch({
             action: "appsDrawer",
@@ -1856,11 +1841,7 @@ export default class RoomView extends React.Component<IProps, IState> {
 
         let aux = null;
         let previewBar;
-        let hideCancel = false;
-        if (this.state.forwardingEvent) {
-            aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
-        } else if (this.state.searching) {
-            hideCancel = true; // has own cancel
+        if (this.state.searching) {
             aux = <SearchBar
                 searchInProgress={this.state.searchInProgress}
                 onCancelClick={this.onCancelSearchClick}
@@ -1869,9 +1850,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             />;
         } else if (showRoomUpgradeBar) {
             aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />;
-            hideCancel = true;
         } else if (this.state.showingPinned) {
-            hideCancel = true; // has own cancel
             aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
         } else if (myMembership !== "join") {
             // We do have a room object for this room, but we're not currently in it.
@@ -1881,7 +1860,6 @@ export default class RoomView extends React.Component<IProps, IState> {
                 inviterName = this.props.oobData.inviterName;
             }
             const invitedEmail = this.props.threepidInvite?.toEmail;
-            hideCancel = true;
             previewBar = (
                 <RoomPreviewBar
                     onJoinClick={this.onJoinButtonClicked}
@@ -1999,11 +1977,8 @@ export default class RoomView extends React.Component<IProps, IState> {
             hideMessagePanel = true;
         }
 
-        const shouldHighlight = this.state.isInitialEventHighlighted;
         let highlightedEventId = null;
-        if (this.state.forwardingEvent) {
-            highlightedEventId = this.state.forwardingEvent.getId();
-        } else if (shouldHighlight) {
+        if (this.state.isInitialEventHighlighted) {
             highlightedEventId = this.state.initialEventId;
         }
 
@@ -2091,7 +2066,6 @@ export default class RoomView extends React.Component<IProps, IState> {
                             onSearchClick={this.onSearchClick}
                             onSettingsClick={this.onSettingsClick}
                             onPinnedClick={this.onPinnedClick}
-                            onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
                             onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
                             onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
                             e2eStatus={this.state.e2eStatus}
diff --git a/src/components/views/rooms/ForwardMessage.js b/src/components/views/rooms/ForwardMessage.js
deleted file mode 100644
index dd894c0dcf..0000000000
--- a/src/components/views/rooms/ForwardMessage.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- Copyright 2017 Vector Creations Ltd
- Copyright 2017 Michael Telatynski
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import { _t } from '../../../languageHandler';
-import {Key} from '../../../Keyboard';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-
-@replaceableComponent("views.rooms.ForwardMessage")
-export default class ForwardMessage extends React.Component {
-    static propTypes = {
-        onCancelClick: PropTypes.func.isRequired,
-    };
-
-    componentDidMount() {
-        document.addEventListener('keydown', this._onKeyDown);
-    }
-
-    componentWillUnmount() {
-        document.removeEventListener('keydown', this._onKeyDown);
-    }
-
-    _onKeyDown = ev => {
-        switch (ev.key) {
-            case Key.ESCAPE:
-                this.props.onCancelClick();
-                break;
-        }
-    };
-
-    render() {
-        return (
-            <div className="mx_ForwardMessage">
-                <h1>{ _t('Please select the destination room for this message') }</h1>
-            </div>
-        );
-    }
-}
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index f856f7f6ef..cd4f89f964 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -22,7 +22,6 @@ import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import RateLimitedFunc from '../../../ratelimitedfunc';
 
-import {CancelButton} from './SimpleRoomHeader';
 import SettingsStore from "../../../settings/SettingsStore";
 import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
 import E2EIcon from './E2EIcon';
@@ -44,7 +43,6 @@ export default class RoomHeader extends React.Component {
         onPinnedClick: PropTypes.func,
         onSearchClick: PropTypes.func,
         onLeaveClick: PropTypes.func,
-        onCancelClick: PropTypes.func,
         e2eStatus: PropTypes.string,
         onAppsClick: PropTypes.func,
         appsShown: PropTypes.bool,
@@ -54,7 +52,6 @@ export default class RoomHeader extends React.Component {
     static defaultProps = {
         editing: false,
         inRoom: false,
-        onCancelClick: null,
     };
 
     componentDidMount() {
@@ -120,13 +117,8 @@ export default class RoomHeader extends React.Component {
 
     render() {
         let searchStatus = null;
-        let cancelButton = null;
         let pinnedEventsButton = null;
 
-        if (this.props.onCancelClick) {
-            cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
-        }
-
         // don't display the search count until the search completes and
         // gives us a valid (possibly zero) searchCount.
         if (this.props.searchInfo &&
@@ -265,7 +257,6 @@ export default class RoomHeader extends React.Component {
                     <div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
                     { name }
                     { topicElement }
-                    { cancelButton }
                     { rightRow }
                     <RoomHeaderButtons />
                 </div>
diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js
index b2a66f6670..02c1e1e8a1 100644
--- a/src/components/views/rooms/SimpleRoomHeader.js
+++ b/src/components/views/rooms/SimpleRoomHeader.js
@@ -16,23 +16,9 @@ limitations under the License.
 
 import React from 'react';
 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) {
-    const {onClick} = props;
-
-    return (
-        <AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
-            <img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
-                width="18" height="18" alt={_t("Cancel")} />
-        </AccessibleButton>
-    );
-}
-
 /*
  * A stripped-down room header used for things like the user settings
  * and room directory.
@@ -41,18 +27,13 @@ export function CancelButton(props) {
 export default class SimpleRoomHeader extends React.Component {
     static propTypes = {
         title: PropTypes.string,
-        onCancelClick: PropTypes.func,
 
         // `src` to a TintableSvg. Optional.
         icon: PropTypes.string,
     };
 
     render() {
-        let cancelButton;
         let icon;
-        if (this.props.onCancelClick) {
-            cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
-        }
         if (this.props.icon) {
             const TintableSvg = sdk.getComponent('elements.TintableSvg');
             icon = <TintableSvg
@@ -67,7 +48,6 @@ export default class SimpleRoomHeader extends React.Component {
                     <div className="mx_RoomHeader_simpleHeader">
                         { icon }
                         { this.props.title }
-                        { cancelButton }
                     </div>
                 </div>
             </div>
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index ed49fb6b44..6f2a4e8aee 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1458,7 +1458,6 @@
     "Encrypting your message...": "Encrypting your message...",
     "Your message was sent": "Your message was sent",
     "Failed to send": "Failed to send",
-    "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",
     "and %(count)s others...|other": "and %(count)s others...",
diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx
index fe2e0a66b2..37162c8ae7 100644
--- a/src/stores/RoomViewStore.tsx
+++ b/src/stores/RoomViewStore.tsx
@@ -53,8 +53,6 @@ const INITIAL_STATE = {
     // Any error that has occurred during loading
     roomLoadError: null,
 
-    forwardingEvent: null,
-
     quotingEvent: null,
 
     replyingToEvent: null,
@@ -149,11 +147,6 @@ class RoomViewStore extends Store<ActionPayload> {
             case 'on_logged_out':
                 this.reset();
                 break;
-            case 'forward_event':
-                this.setState({
-                    forwardingEvent: payload.event,
-                });
-                break;
             case 'reply_to_event':
                 // If currently viewed room does not match the room in which we wish to reply then change rooms
                 // this can happen when performing a search across all rooms
@@ -186,7 +179,6 @@ class RoomViewStore extends Store<ActionPayload> {
                 roomAlias: payload.room_alias,
                 initialEventId: payload.event_id,
                 isInitialEventHighlighted: payload.highlighted,
-                forwardingEvent: null,
                 roomLoading: false,
                 roomLoadError: null,
                 // should peek by default
@@ -206,14 +198,6 @@ class RoomViewStore extends Store<ActionPayload> {
                 newState.replyingToEvent = payload.replyingToEvent;
             }
 
-            if (this.state.forwardingEvent) {
-                dis.dispatch({
-                    action: 'send_event',
-                    room_id: newState.roomId,
-                    event: this.state.forwardingEvent,
-                });
-            }
-
             this.setState(newState);
 
             if (payload.auto_join) {
@@ -419,11 +403,6 @@ class RoomViewStore extends Store<ActionPayload> {
         return this.state.joinError;
     }
 
-    // The mxEvent if one is about to be forwarded
-    public getForwardingEvent() {
-        return this.state.forwardingEvent;
-    }
-
     // The mxEvent if one is currently being replied to/quoted
     public getQuotingEvent() {
         return this.state.replyingToEvent;

From 219c983d191d9172e3c68aa11b1765f91ac5c6cd Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sun, 9 May 2021 10:58:44 -0400
Subject: [PATCH 013/159] Use import instead of sdk.getComponent

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/context_menus/MessageContextMenu.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index e069bba975..fc2c366b07 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -31,6 +31,7 @@ import { isContentActionable } from '../../../utils/EventUtils';
 import {MenuItem} from "../../structures/ContextMenu";
 import {EventType} from "matrix-js-sdk/src/@types/event";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import ForwardDialog from "../dialogs/ForwardDialog";
 
 export function canCancel(eventStatus) {
     return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@@ -154,7 +155,6 @@ export default class MessageContextMenu extends React.Component {
     };
 
     onForwardClick = () => {
-        const ForwardDialog = sdk.getComponent("dialogs.ForwardDialog");
         Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
             cli: MatrixClientPeg.get(),
             event: this.props.mxEvent,

From c96888c9cba48dfb1d318b2cfb89cd699c6a8529 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 00:38:01 -0400
Subject: [PATCH 014/159] Make ForwardDialog more readable

Signed-off-by: Robin Townsend <robin@robin.town>
---
 .../views/dialogs/ForwardDialog.tsx           | 21 ++++++++++---------
 1 file changed, 11 insertions(+), 10 deletions(-)

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index ce38a783b3..b1f558dfef 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -39,6 +39,7 @@ const AVATAR_SIZE = 30;
 
 interface IProps extends IDialogProps {
     cli: MatrixClient;
+    // The event to forward
     event: MatrixEvent;
 }
 
@@ -116,7 +117,7 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, onFinished }) => {
     });
     mockEvent.sender = {
         name: profileInfo.displayname || userId,
-        userId: userId,
+        userId,
         getAvatarUrl: (..._) => {
             return avatarUrlForUser(
                 { avatarUrl: profileInfo.avatar_url },
@@ -179,28 +180,28 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, onFinished }) => {
                 { rooms.length > 0 ? (
                     <div className="mx_ForwardList_section">
                         <h3>{ _t("Rooms") }</h3>
-                        { rooms.map(room => {
-                            return <Entry
+                        { rooms.map(room =>
+                            <Entry
                                 key={room.roomId}
                                 room={room}
                                 send={() => cli.sendEvent(room.roomId, event.getType(), event.getContent())}
-                            />;
-                        }) }
+                            />,
+                        ) }
                     </div>
                 ) : undefined }
 
                 { dms.length > 0 ? (
                     <div className="mx_ForwardList_section">
                         <h3>{ _t("Direct Messages") }</h3>
-                        { dms.map(room => {
-                            return <Entry
+                        { dms.map(room =>
+                            <Entry
                                 key={room.roomId}
                                 room={room}
                                 send={() => cli.sendEvent(room.roomId, event.getType(), event.getContent())}
-                            />;
-                        }) }
+                            />,
+                        ) }
                     </div>
-                ) : null }
+                ) : undefined }
 
                 { rooms.length + dms.length < 1 ? <span className="mx_ForwardList_noResults">
                     { _t("No results") }

From 100efb1a90c92ac6a8771787cd2564f218507550 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 00:40:54 -0400
Subject: [PATCH 015/159] Fix ForwardDialog crashing when rendering reply

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/context_menus/MessageContextMenu.js | 1 +
 src/components/views/dialogs/ForwardDialog.tsx           | 9 +++++++--
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index fc2c366b07..002542bf88 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -158,6 +158,7 @@ export default class MessageContextMenu extends React.Component {
         Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
             cli: MatrixClientPeg.get(),
             event: this.props.mxEvent,
+            permalinkCreator: this.props.permalinkCreator,
         }, 'mx_Dialog_forwardmessage');
         this.closeMenu();
     };
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index b1f558dfef..5cb5d68745 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -33,6 +33,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
 import AccessibleButton from "../elements/AccessibleButton";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 import DMRoomMap from "../../../utils/DMRoomMap";
+import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
 import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
 
 const AVATAR_SIZE = 30;
@@ -41,6 +42,9 @@ interface IProps extends IDialogProps {
     cli: MatrixClient;
     // The event to forward
     event: MatrixEvent;
+    // We need a permalink creator for the source room to pass through to EventTile
+    // in case the event is a reply (even though the user can't get at the link)
+    permalinkCreator: RoomPermalinkCreator;
 }
 
 interface IEntryProps {
@@ -97,7 +101,7 @@ const Entry: React.FC<IEntryProps> = ({ room, send }) => {
     </div>;
 };
 
-const ForwardDialog: React.FC<IProps> = ({ cli, event, onFinished }) => {
+const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinished }) => {
     const userId = cli.getUserId();
     const [profileInfo, setProfileInfo] = useState<any>({});
     useEffect(() => {
@@ -113,7 +117,7 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, onFinished }) => {
             age: 97,
         },
         event_id: "$9999999999999999999999999999999999999999999",
-        room_id: "!999999999999999999:example.org",
+        room_id: event.getRoomId(),
     });
     mockEvent.sender = {
         name: profileInfo.displayname || userId,
@@ -165,6 +169,7 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, onFinished }) => {
                 mxEvent={mockEvent}
                 layout={previewLayout}
                 enableFlair={SettingsStore.getValue(UIFeature.Flair)}
+                permalinkCreator={permalinkCreator}
             />
         </div>
         <div className="mx_ForwardList">

From eb07f1fb86de32944b8a0375beec89abdeef63d0 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 00:54:00 -0400
Subject: [PATCH 016/159] Test that ForwardDialog can render replies

Previously ForwardDialog was not giving its EventTile message preview
the information it needed to render a ReplyThread. This was a bit tricky
to fix since we were pulling a fake event out of thin air, so this
ensures it doesn't regress.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 .../views/dialogs/ForwardDialog-test.js       | 64 ++++++++++++++-----
 test/test-utils.js                            |  1 +
 2 files changed, 49 insertions(+), 16 deletions(-)

diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
index cabcbb0325..af325ab8f0 100644
--- a/test/components/views/dialogs/ForwardDialog-test.js
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -24,44 +24,51 @@ import {act} from "react-dom/test-utils";
 import * as TestUtils from "../../../test-utils";
 import {MatrixClientPeg} from "../../../../src/MatrixClientPeg";
 import DMRoomMap from "../../../../src/utils/DMRoomMap";
+import {RoomPermalinkCreator} from "../../../../src/utils/permalinks/Permalinks";
 import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog";
 
 configure({ adapter: new Adapter() });
 
 describe("ForwardDialog", () => {
-    let client;
-    let wrapper;
-
-    const message = TestUtils.mkMessage({
-        room: "!111111111111111111:example.org",
+    const sourceRoom = "!111111111111111111:example.org";
+    const defaultMessage = TestUtils.mkMessage({
+        room: sourceRoom,
         user: "@alice:example.org",
         msg: "Hello world!",
         event: true,
     });
+    const defaultRooms = ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name));
 
-    beforeEach(async () => {
-        TestUtils.stubClient();
-        DMRoomMap.makeShared();
-        client = MatrixClientPeg.get();
-        client.getVisibleRooms = jest.fn().mockReturnValue(
-            ["a", "A", "b"].map(name => TestUtils.mkStubRoom(name, name)),
-        );
-        client.getUserId = jest.fn().mockReturnValue("@bob:example.org");
+    const mountForwardDialog = async (message = defaultMessage, rooms = defaultRooms) => {
+        const client = MatrixClientPeg.get();
+        client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
 
+        let wrapper;
         await act(async () => {
             wrapper = mount(
                 <ForwardDialog
                     cli={client}
                     event={message}
+                    permalinkCreator={new RoomPermalinkCreator(undefined, sourceRoom)}
                     onFinished={jest.fn()}
                 />,
             );
             // Wait one tick for our profile data to load so the state update happens within act
             await new Promise(resolve => setImmediate(resolve));
         });
+
+        return wrapper;
+    };
+
+    beforeEach(() => {
+        TestUtils.stubClient();
+        DMRoomMap.makeShared();
+        MatrixClientPeg.get().getUserId = jest.fn().mockReturnValue("@bob:example.org");
     });
 
-    it("shows a preview with us as the sender", () => {
+    it("shows a preview with us as the sender", async () => {
+        const wrapper = await mountForwardDialog();
+
         const previewBody = wrapper.find(".mx_EventTile_body");
         expect(previewBody.text()).toBe("Hello world!");
 
@@ -70,7 +77,9 @@ describe("ForwardDialog", () => {
         expect(previewAvatar.prop("title")).toBe("@bob:example.org");
     });
 
-    it("filters the rooms", () => {
+    it("filters the rooms", async () => {
+        const wrapper = await mountForwardDialog();
+
         const roomsBefore = wrapper.find(".mx_ForwardList_entry");
         expect(roomsBefore).toHaveLength(3);
 
@@ -83,10 +92,12 @@ describe("ForwardDialog", () => {
     });
 
     it("tracks message sending progress across multiple rooms", async () => {
+        const wrapper = await mountForwardDialog();
+
         // Make sendEvent require manual resolution so we can see the sending state
         let finishSend;
         let cancelSend;
-        client.sendEvent = jest.fn(() => new Promise((resolve, reject) => {
+        MatrixClientPeg.get().sendEvent = jest.fn(() => new Promise((resolve, reject) => {
             finishSend = resolve;
             cancelSend = reject;
         }));
@@ -121,4 +132,25 @@ describe("ForwardDialog", () => {
         });
         expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sent");
     });
+
+    it("can render replies", async () => {
+        const replyMessage = TestUtils.mkEvent({
+            type: "m.room.message",
+            room: "!111111111111111111:example.org",
+            user: "@alice:example.org",
+            content: {
+                msgtype: "m.text",
+                body: "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
+                "m.relates_to": {
+                    "m.in_reply_to": {
+                        event_id: "$2222222222222222222222222222222222222222222",
+                    },
+                },
+            },
+            event: true,
+        });
+
+        const wrapper = await mountForwardDialog(replyMessage);
+        expect(wrapper.find("ReplyThread")).toBeTruthy();
+    });
 });
diff --git a/test/test-utils.js b/test/test-utils.js
index 985e9f804b..8c9c62844a 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -236,6 +236,7 @@ export function mkStubRoom(roomId = null, name) {
         getPendingEvents: () => [],
         getLiveTimeline: () => stubTimeline,
         getUnfilteredTimelineSet: () => null,
+        findEventById: () => null,
         getAccountData: () => null,
         hasMembershipState: () => null,
         getVersion: () => '1',

From 35cf0e1c7efbd4f627e8fcc00cd8f8b96daf1b8e Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 01:02:12 -0400
Subject: [PATCH 017/159] Find components by name rather than class in
 ForwardDialog test

It makes things shorter and more readable!

Signed-off-by: Robin Townsend <robin@robin.town>
---
 .../views/dialogs/ForwardDialog-test.js       | 32 ++++++++-----------
 1 file changed, 13 insertions(+), 19 deletions(-)

diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
index af325ab8f0..f181157d09 100644
--- a/test/components/views/dialogs/ForwardDialog-test.js
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -80,15 +80,13 @@ describe("ForwardDialog", () => {
     it("filters the rooms", async () => {
         const wrapper = await mountForwardDialog();
 
-        const roomsBefore = wrapper.find(".mx_ForwardList_entry");
-        expect(roomsBefore).toHaveLength(3);
+        expect(wrapper.find("Entry")).toHaveLength(3);
 
-        const searchInput = wrapper.find(".mx_SearchBox input");
+        const searchInput = wrapper.find("SearchBox input");
         searchInput.instance().value = "a";
         searchInput.simulate("change");
 
-        const roomsAfter = wrapper.find(".mx_ForwardList_entry");
-        expect(roomsAfter).toHaveLength(2);
+        expect(wrapper.find("Entry")).toHaveLength(2);
     });
 
     it("tracks message sending progress across multiple rooms", async () => {
@@ -102,35 +100,31 @@ describe("ForwardDialog", () => {
             cancelSend = reject;
         }));
 
-        const firstRoom = wrapper.find(".mx_ForwardList_entry").first();
-        expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Send");
+        const firstButton = wrapper.find("Entry AccessibleButton").first();
+        expect(firstButton.text()).toBe("Send");
 
-        act(() => {
-            firstRoom.find(".mx_AccessibleButton").simulate("click");
-        });
-        expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Sending…");
+        act(() => { firstButton.simulate("click"); });
+        expect(firstButton.text()).toBe("Sending…");
 
         await act(async () => {
             cancelSend();
             // Wait one tick for the button to realize the send failed
             await new Promise(resolve => setImmediate(resolve));
         });
-        expect(firstRoom.find(".mx_AccessibleButton").text()).toBe("Failed to send");
+        expect(firstButton.text()).toBe("Failed to send");
 
-        const secondRoom = wrapper.find(".mx_ForwardList_entry").at(1);
-        expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Send");
+        const secondButton = wrapper.find("Entry AccessibleButton").at(1);
+        expect(secondButton.text()).toBe("Send");
 
-        act(() => {
-            secondRoom.find(".mx_AccessibleButton").simulate("click");
-        });
-        expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sending…");
+        act(() => { secondButton.simulate("click"); });
+        expect(secondButton.text()).toBe("Sending…");
 
         await act(async () => {
             finishSend();
             // Wait one tick for the button to realize the send succeeded
             await new Promise(resolve => setImmediate(resolve));
         });
-        expect(secondRoom.find(".mx_AccessibleButton").text()).toBe("Sent");
+        expect(secondButton.text()).toBe("Sent");
     });
 
     it("can render replies", async () => {

From 09ba74a8514fd907e758f1af24c795fa78ce8643 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 01:04:25 -0400
Subject: [PATCH 018/159] Disable forward buttons for rooms without send
 permissions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

…and add a tooltip to explain why they can't accept forwarded messages.
It was chosen to disable the buttons rather than hide the entries from
the list, since hiding them without explanation could cause confusion.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 .../views/dialogs/ForwardDialog.tsx           | 76 ++++++++++++-------
 1 file changed, 47 insertions(+), 29 deletions(-)

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 5cb5d68745..fd10a88d93 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -31,6 +31,7 @@ import EventTile from "../rooms/EventTile";
 import SearchBox from "../../structures/SearchBox";
 import RoomAvatar from "../avatars/RoomAvatar";
 import AccessibleButton from "../elements/AccessibleButton";
+import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 import DMRoomMap from "../../../utils/DMRoomMap";
 import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
@@ -63,41 +64,58 @@ enum SendState {
 const Entry: React.FC<IEntryProps> = ({ room, send }) => {
     const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
 
-    let label;
-    let className;
-    if (sendState === SendState.CanSend) {
-        label = _t("Send");
-        className = "mx_ForwardList_canSend";
-    } else if (sendState === SendState.Sending) {
-        label = _t("Sending…");
-        className = "mx_ForwardList_sending";
-    } else if (sendState === SendState.Sent) {
-        label = _t("Sent");
-        className = "mx_ForwardList_sent";
+    let button;
+    if (room.maySendMessage()) {
+        let label;
+        let className;
+        if (sendState === SendState.CanSend) {
+            label = _t("Send");
+            className = "mx_ForwardList_canSend";
+        } else if (sendState === SendState.Sending) {
+            label = _t("Sending…");
+            className = "mx_ForwardList_sending";
+        } else if (sendState === SendState.Sent) {
+            label = _t("Sent");
+            className = "mx_ForwardList_sent";
+        } else {
+            label = _t("Failed to send");
+            className = "mx_ForwardList_sendFailed";
+        }
+
+        button =
+            <AccessibleButton
+                kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
+                className={className}
+                onClick={async () => {
+                    setSendState(SendState.Sending);
+                    try {
+                        await send();
+                        setSendState(SendState.Sent);
+                    } catch (e) {
+                        setSendState(SendState.Failed);
+                    }
+                }}
+                disabled={sendState !== SendState.CanSend}
+            >
+                { label }
+            </AccessibleButton>;
     } else {
-        label = _t("Failed to send");
-        className = "mx_ForwardList_sendFailed";
+        button =
+            <AccessibleTooltipButton
+                kind="primary_outline"
+                className={"mx_ForwardList_canSend"}
+                onClick={() => {}}
+                disabled={true}
+                title={_t("You do not have permission to post to this room")}
+            >
+                { _t("Send") }
+            </AccessibleTooltipButton>;
     }
 
     return <div className="mx_ForwardList_entry">
         <RoomAvatar room={room} height={32} width={32} />
         <span className="mx_ForwardList_entry_name">{ room.name }</span>
-        <AccessibleButton
-            kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
-            className={className}
-            onClick={async () => {
-                setSendState(SendState.Sending);
-                try {
-                    await send();
-                    setSendState(SendState.Sent);
-                } catch (e) {
-                    setSendState(SendState.Failed);
-                }
-            }}
-            disabled={sendState !== SendState.CanSend}
-        >
-            { label }
-        </AccessibleButton>
+        { button }
     </div>;
 };
 

From eb779cd3d8db279e9cf91134b1aa5bbf4326662c Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 01:07:42 -0400
Subject: [PATCH 019/159] Test that forward buttons are disabled for rooms
 without permission

Signed-off-by: Robin Townsend <robin@robin.town>
---
 test/components/views/dialogs/ForwardDialog-test.js | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
index f181157d09..706d47acdc 100644
--- a/test/components/views/dialogs/ForwardDialog-test.js
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -147,4 +147,17 @@ describe("ForwardDialog", () => {
         const wrapper = await mountForwardDialog(replyMessage);
         expect(wrapper.find("ReplyThread")).toBeTruthy();
     });
+
+    it("disables buttons for rooms without send permissions", async () => {
+        const readOnlyRoom = TestUtils.mkStubRoom("a", "a");
+        readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false);
+        const rooms = [readOnlyRoom, TestUtils.mkStubRoom("b", "b")];
+
+        const wrapper = await mountForwardDialog(undefined, rooms);
+
+        const firstButton = wrapper.find("Entry AccessibleButton").first();
+        expect(firstButton.prop("disabled")).toBe(true);
+        const secondButton = wrapper.find("Entry AccessibleButton").last();
+        expect(secondButton.prop("disabled")).toBe(false);
+    });
 });

From 5c10e1e5744e7c8b753304e4994c172f4ba493c3 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 01:16:37 -0400
Subject: [PATCH 020/159] Fix lints

Signed-off-by: Robin Townsend <robin@robin.town>
---
 test/components/views/dialogs/ForwardDialog-test.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
index 706d47acdc..2f062a84cc 100644
--- a/test/components/views/dialogs/ForwardDialog-test.js
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -133,8 +133,8 @@ describe("ForwardDialog", () => {
             room: "!111111111111111111:example.org",
             user: "@alice:example.org",
             content: {
-                msgtype: "m.text",
-                body: "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
+                "msgtype": "m.text",
+                "body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
                 "m.relates_to": {
                     "m.in_reply_to": {
                         event_id: "$2222222222222222222222222222222222222222222",

From bfba2b0b6f09b7d470a6274d07053d0d854f7cd0 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 11:16:51 -0400
Subject: [PATCH 021/159] Push ForwardDialog scrollbar into the gutter
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

…to give more space between it and the buttons.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index a3aa08d04f..a447b93cae 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -48,6 +48,8 @@ limitations under the License.
 
         .mx_ForwardList_content {
             flex-grow: 1;
+            // Push the scrollbar into the gutter
+            margin-right: -6px;
         }
 
         .mx_ForwardList_noResults {
@@ -56,6 +58,8 @@ limitations under the License.
         }
 
         .mx_ForwardList_section {
+            margin-right: 6px;
+
             &:not(:first-child) {
                 margin-top: 24px;
             }

From 503301aa89e3a8378d9e48c95b37ec19ab4d5e89 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 13:00:06 -0400
Subject: [PATCH 022/159] Make rooms in ForwardDialog clickable
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

…so that you can jump to a room easily once you've forwarded a message
there.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss     | 29 ++++++----
 .../views/dialogs/ForwardDialog.tsx           | 53 ++++++++++++-------
 .../views/dialogs/ForwardDialog-test.js       | 10 ++--
 3 files changed, 58 insertions(+), 34 deletions(-)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index a447b93cae..30a4cfcacf 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -74,23 +74,30 @@ limitations under the License.
 
             .mx_ForwardList_entry {
                 display: flex;
+                justify-content: space-between;
                 margin-top: 12px;
 
-                .mx_BaseAvatar {
+                .mx_ForwardList_roomButton {
+                    display: flex;
                     margin-right: 12px;
-                }
-
-                .mx_ForwardList_entry_name {
-                    font-size: $font-15px;
-                    line-height: 30px;
                     flex-grow: 1;
-                    overflow: hidden;
-                    white-space: nowrap;
-                    text-overflow: ellipsis;
-                    margin-right: 12px;
+                    min-width: 0;
+
+                    .mx_BaseAvatar {
+                        margin-right: 12px;
+                    }
+
+                    .mx_ForwardList_entry_name {
+                        font-size: $font-15px;
+                        line-height: 30px;
+                        overflow: hidden;
+                        white-space: nowrap;
+                        text-overflow: ellipsis;
+                        margin-right: 12px;
+                    }
                 }
 
-                .mx_AccessibleButton {
+                .mx_ForwardList_sendButton {
                     &.mx_ForwardList_sending, &.mx_ForwardList_sent {
                         &::before {
                             content: '';
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index fd10a88d93..ed6973cd4a 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -21,6 +21,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
 import {MatrixClient} from "matrix-js-sdk/src/client";
 
 import {_t} from "../../../languageHandler";
+import dis from "../../../dispatcher/dispatcher";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
 import {Layout} from "../../../settings/Layout";
@@ -50,8 +51,9 @@ interface IProps extends IDialogProps {
 
 interface IEntryProps {
     room: Room;
-    // Callback to forward the message to this room
-    send(): Promise<void>;
+    event: MatrixEvent;
+    cli: MatrixClient;
+    onFinished(success: boolean): void;
 }
 
 enum SendState {
@@ -61,9 +63,26 @@ enum SendState {
     Failed,
 }
 
-const Entry: React.FC<IEntryProps> = ({ room, send }) => {
+const Entry: React.FC<IEntryProps> = ({ room, event, cli, onFinished }) => {
     const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
 
+    const jumpToRoom = () => {
+        dis.dispatch({
+            action: "view_room",
+            room_id: room.roomId,
+        });
+        onFinished(true);
+    };
+    const send = async () => {
+        setSendState(SendState.Sending);
+        try {
+            await cli.sendEvent(room.roomId, event.getType(), event.getContent());
+            setSendState(SendState.Sent);
+        } catch (e) {
+            setSendState(SendState.Failed);
+        }
+    };
+
     let button;
     if (room.maySendMessage()) {
         let label;
@@ -85,16 +104,8 @@ const Entry: React.FC<IEntryProps> = ({ room, send }) => {
         button =
             <AccessibleButton
                 kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
-                className={className}
-                onClick={async () => {
-                    setSendState(SendState.Sending);
-                    try {
-                        await send();
-                        setSendState(SendState.Sent);
-                    } catch (e) {
-                        setSendState(SendState.Failed);
-                    }
-                }}
+                className={`mx_ForwardList_sendButton ${className}`}
+                onClick={send}
                 disabled={sendState !== SendState.CanSend}
             >
                 { label }
@@ -103,7 +114,7 @@ const Entry: React.FC<IEntryProps> = ({ room, send }) => {
         button =
             <AccessibleTooltipButton
                 kind="primary_outline"
-                className={"mx_ForwardList_canSend"}
+                className="mx_ForwardList_sendButton mx_ForwardList_canSend"
                 onClick={() => {}}
                 disabled={true}
                 title={_t("You do not have permission to post to this room")}
@@ -113,8 +124,10 @@ const Entry: React.FC<IEntryProps> = ({ room, send }) => {
     }
 
     return <div className="mx_ForwardList_entry">
-        <RoomAvatar room={room} height={32} width={32} />
-        <span className="mx_ForwardList_entry_name">{ room.name }</span>
+        <AccessibleButton className="mx_ForwardList_roomButton" onClick={jumpToRoom}>
+            <RoomAvatar room={room} height={32} width={32} />
+            <span className="mx_ForwardList_entry_name">{ room.name }</span>
+        </AccessibleButton>
         { button }
     </div>;
 };
@@ -207,7 +220,9 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinis
                             <Entry
                                 key={room.roomId}
                                 room={room}
-                                send={() => cli.sendEvent(room.roomId, event.getType(), event.getContent())}
+                                event={event}
+                                cli={cli}
+                                onFinished={onFinished}
                             />,
                         ) }
                     </div>
@@ -220,7 +235,9 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinis
                             <Entry
                                 key={room.roomId}
                                 room={room}
-                                send={() => cli.sendEvent(room.roomId, event.getType(), event.getContent())}
+                                event={event}
+                                cli={cli}
+                                onFinished={onFinished}
                             />,
                         ) }
                     </div>
diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
index 2f062a84cc..331ee9d131 100644
--- a/test/components/views/dialogs/ForwardDialog-test.js
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -100,7 +100,7 @@ describe("ForwardDialog", () => {
             cancelSend = reject;
         }));
 
-        const firstButton = wrapper.find("Entry AccessibleButton").first();
+        const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
         expect(firstButton.text()).toBe("Send");
 
         act(() => { firstButton.simulate("click"); });
@@ -113,8 +113,8 @@ describe("ForwardDialog", () => {
         });
         expect(firstButton.text()).toBe("Failed to send");
 
-        const secondButton = wrapper.find("Entry AccessibleButton").at(1);
-        expect(secondButton.text()).toBe("Send");
+        const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1);
+        expect(secondButton.render().text()).toBe("Send");
 
         act(() => { secondButton.simulate("click"); });
         expect(secondButton.text()).toBe("Sending…");
@@ -155,9 +155,9 @@ describe("ForwardDialog", () => {
 
         const wrapper = await mountForwardDialog(undefined, rooms);
 
-        const firstButton = wrapper.find("Entry AccessibleButton").first();
+        const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
         expect(firstButton.prop("disabled")).toBe(true);
-        const secondButton = wrapper.find("Entry AccessibleButton").last();
+        const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last();
         expect(secondButton.prop("disabled")).toBe(false);
     });
 });

From 7efbd2d930cc93dffdabe10f84bc320f58cf23c5 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 10 May 2021 13:57:47 -0400
Subject: [PATCH 023/159] Hide unencrypted badge from ForwardDialog preview

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index 30a4cfcacf..9bd0bd2879 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -35,6 +35,12 @@ limitations under the License.
         .mx_EventTile_msgOption {
             display: none;
         }
+
+        // When forwarding messages from encrypted rooms, EventTile will complain
+        // that our preview is unencrypted, which doesn't actually matter
+        .mx_EventTile_e2eIcon_unencrypted {
+            display: none;
+        }
     }
 
     .mx_ForwardList {

From e798b36f1db8ea73c81fcc1626e1af31014c0800 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sun, 16 May 2021 08:39:22 -0400
Subject: [PATCH 024/159] Decorate forward dialog room avatars

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss      | 2 +-
 src/components/views/dialogs/ForwardDialog.tsx | 4 ++--
 test/test-utils.js                             | 1 +
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index 9bd0bd2879..2a72ea4ffb 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -89,7 +89,7 @@ limitations under the License.
                     flex-grow: 1;
                     min-width: 0;
 
-                    .mx_BaseAvatar {
+                    .mx_DecoratedRoomAvatar {
                         margin-right: 12px;
                     }
 
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index ed6973cd4a..1ef4841851 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -30,7 +30,7 @@ import BaseDialog from "./BaseDialog";
 import {avatarUrlForUser} from "../../../Avatar";
 import EventTile from "../rooms/EventTile";
 import SearchBox from "../../structures/SearchBox";
-import RoomAvatar from "../avatars/RoomAvatar";
+import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
 import AccessibleButton from "../elements/AccessibleButton";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
@@ -125,7 +125,7 @@ const Entry: React.FC<IEntryProps> = ({ room, event, cli, onFinished }) => {
 
     return <div className="mx_ForwardList_entry">
         <AccessibleButton className="mx_ForwardList_roomButton" onClick={jumpToRoom}>
-            <RoomAvatar room={room} height={32} width={32} />
+            <DecoratedRoomAvatar room={room} avatarSize={32} />
             <span className="mx_ForwardList_entry_name">{ room.name }</span>
         </AccessibleButton>
         { button }
diff --git a/test/test-utils.js b/test/test-utils.js
index 8c9c62844a..a2ccf84728 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -253,6 +253,7 @@ export function mkStubRoom(roomId = null, name) {
         tags: {},
         setBlacklistUnverifiedDevices: jest.fn(),
         on: jest.fn(),
+        off: jest.fn(),
         removeListener: jest.fn(),
         getDMInviter: jest.fn(),
         name,

From 83224dc7b66b6d3b51d6d58568759611726b0d04 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Wed, 19 May 2021 13:32:27 -0400
Subject: [PATCH 025/159] Ensure forward list room decorations are aligned

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index 2a72ea4ffb..6fcd7d881a 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -82,6 +82,7 @@ limitations under the License.
                 display: flex;
                 justify-content: space-between;
                 margin-top: 12px;
+                height: 32px;
 
                 .mx_ForwardList_roomButton {
                     display: flex;

From 6cb6c7f3d0c538ac5e0742c806849d33ec685631 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Wed, 19 May 2021 13:33:48 -0400
Subject: [PATCH 026/159] Combine forward dialog room and DM lists

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss     |  2 +-
 .../views/dialogs/ForwardDialog.tsx           | 46 ++++---------------
 2 files changed, 9 insertions(+), 39 deletions(-)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index 6fcd7d881a..47ab219150 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -63,7 +63,7 @@ limitations under the License.
             margin-top: 24px;
         }
 
-        .mx_ForwardList_section {
+        .mx_ForwardList_results {
             margin-right: 6px;
 
             &:not(:first-child) {
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 1ef4841851..b7a176cfce 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -34,7 +34,6 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
 import AccessibleButton from "../elements/AccessibleButton";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
-import DMRoomMap from "../../../utils/DMRoomMap";
 import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
 import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
 
@@ -162,26 +161,15 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinis
         getMxcAvatarUrl: () => profileInfo.avatar_url,
     };
 
-    const visibleRooms = useMemo(() => sortRooms(
+    const [query, setQuery] = useState("");
+    const lcQuery = query.toLowerCase();
+
+    const rooms = useMemo(() => sortRooms(
         cli.getVisibleRooms().filter(
             room => room.getMyMembership() === "join" &&
                 !(SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()),
         ),
-    ), [cli]);
-
-    const [query, setQuery] = useState("");
-    const lcQuery = query.toLowerCase();
-
-    const [rooms, dms] = visibleRooms.reduce((arr, room) => {
-        if (room.name.toLowerCase().includes(lcQuery)) {
-            if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
-                arr[1].push(room);
-            } else {
-                arr[0].push(room);
-            }
-        }
-        return arr;
-    }, [[], []]);
+    ), [cli]).filter(room => room.name.toLowerCase().includes(lcQuery));
 
     const previewLayout = SettingsStore.getValue("layout");
 
@@ -214,8 +202,7 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinis
             />
             <AutoHideScrollbar className="mx_ForwardList_content" id="mx_ForwardList">
                 { rooms.length > 0 ? (
-                    <div className="mx_ForwardList_section">
-                        <h3>{ _t("Rooms") }</h3>
+                    <div className="mx_ForwardList_results">
                         { rooms.map(room =>
                             <Entry
                                 key={room.roomId}
@@ -226,26 +213,9 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinis
                             />,
                         ) }
                     </div>
-                ) : undefined }
-
-                { dms.length > 0 ? (
-                    <div className="mx_ForwardList_section">
-                        <h3>{ _t("Direct Messages") }</h3>
-                        { dms.map(room =>
-                            <Entry
-                                key={room.roomId}
-                                room={room}
-                                event={event}
-                                cli={cli}
-                                onFinished={onFinished}
-                            />,
-                        ) }
-                    </div>
-                ) : undefined }
-
-                { rooms.length + dms.length < 1 ? <span className="mx_ForwardList_noResults">
+                ) : <span className="mx_ForwardList_noResults">
                     { _t("No results") }
-                </span> : undefined }
+                </span> }
             </AutoHideScrollbar>
         </div>
     </BaseDialog>;

From 7a045021514788f67c52bf6324e9bf16fceb42cc Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Fri, 21 May 2021 12:41:29 -0400
Subject: [PATCH 027/159] Iterate on forward dialog design feedback

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss     | 80 ++++++++++------
 .../views/dialogs/ForwardDialog.tsx           | 93 +++++++++++--------
 .../elements/AccessibleTooltipButton.tsx      |  6 +-
 src/i18n/strings/en_EN.json                   |  6 +-
 .../views/dialogs/ForwardDialog-test.js       | 12 +--
 5 files changed, 116 insertions(+), 81 deletions(-)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index 47ab219150..1eb37e09da 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -23,7 +23,15 @@ limitations under the License.
     min-height: 0;
     height: 80vh;
 
-    .mx_ForwardDialog_preview {
+    > h3 {
+        margin: 0 0 6px;
+        color: $secondary-fg-color;
+        font-size: $font-12px;
+        font-weight: $font-semi-bold;
+        line-height: $font-15px;
+    }
+
+    > .mx_ForwardDialog_preview {
         max-height: 30%;
         flex-shrink: 0;
         overflow: scroll;
@@ -43,7 +51,14 @@ limitations under the License.
         }
     }
 
-    .mx_ForwardList {
+    > hr {
+        width: 100%;
+        border: none;
+        border-top: 1px solid $input-border-color;
+        margin: 12px 0;
+    }
+
+    > .mx_ForwardList {
         display: contents;
 
         .mx_SearchBox {
@@ -54,8 +69,6 @@ limitations under the License.
 
         .mx_ForwardList_content {
             flex-grow: 1;
-            // Push the scrollbar into the gutter
-            margin-right: -6px;
         }
 
         .mx_ForwardList_noResults {
@@ -64,30 +77,24 @@ limitations under the License.
         }
 
         .mx_ForwardList_results {
-            margin-right: 6px;
-
             &:not(:first-child) {
                 margin-top: 24px;
             }
 
-            > h3 {
-                margin: 0;
-                color: $secondary-fg-color;
-                font-size: $font-12px;
-                font-weight: $font-semi-bold;
-                line-height: $font-15px;
-            }
-
             .mx_ForwardList_entry {
                 display: flex;
                 justify-content: space-between;
-                margin-top: 12px;
                 height: 32px;
+                padding: 6px;
+                border-radius: 8px;
+
+                &:hover {
+                    background-color: $groupFilterPanel-bg-color;
+                }
 
                 .mx_ForwardList_roomButton {
                     display: flex;
                     margin-right: 12px;
-                    flex-grow: 1;
                     min-width: 0;
 
                     .mx_DecoratedRoomAvatar {
@@ -105,26 +112,39 @@ limitations under the License.
                 }
 
                 .mx_ForwardList_sendButton {
-                    &.mx_ForwardList_sending, &.mx_ForwardList_sent {
-                        &::before {
-                            content: '';
-                            display: inline-block;
-                            background-color: $button-primary-bg-color;
-                            mask-position: center;
-                            mask-repeat: no-repeat;
-                            mask-size: 14px;
-                            width: 14px;
-                            height: 14px;
-                            margin-right: 5px;
-                        }
+                    position: relative;
+
+                    &:not(.mx_ForwardList_canSend) .mx_ForwardList_sendLabel {
+                        // Hide the "Send" label while preserving button size
+                        visibility: hidden;
                     }
 
-                    &.mx_ForwardList_sending::before {
+                    .mx_ForwardList_sendIcon, .mx_NotificationBadge {
+                        position: absolute;
+                    }
+
+                    .mx_NotificationBadge {
+                        opacity: 0.4;
+                    }
+
+                    &.mx_ForwardList_sending .mx_ForwardList_sendIcon {
+                        background-color: $button-primary-bg-color;
                         mask-image: url('$(res)/img/element-icons/circle-sending.svg');
+                        mask-position: center;
+                        mask-repeat: no-repeat;
+                        mask-size: 14px;
+                        width: 14px;
+                        height: 14px;
                     }
 
-                    &.mx_ForwardList_sent::before {
+                    &.mx_ForwardList_sent .mx_ForwardList_sendIcon {
+                        background-color: $button-primary-bg-color;
                         mask-image: url('$(res)/img/element-icons/circle-sent.svg');
+                        mask-position: center;
+                        mask-repeat: no-repeat;
+                        mask-size: 14px;
+                        width: 14px;
+                        height: 14px;
                     }
                 }
             }
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index b7a176cfce..3448e636d9 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -31,9 +31,11 @@ import {avatarUrlForUser} from "../../../Avatar";
 import EventTile from "../rooms/EventTile";
 import SearchBox from "../../structures/SearchBox";
 import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
-import AccessibleButton from "../elements/AccessibleButton";
+import {Alignment} from '../elements/Tooltip';
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
+import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
+import NotificationBadge from "../rooms/NotificationBadge";
 import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
 import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
 
@@ -82,52 +84,60 @@ const Entry: React.FC<IEntryProps> = ({ room, event, cli, onFinished }) => {
         }
     };
 
-    let button;
-    if (room.maySendMessage()) {
-        let label;
-        let className;
-        if (sendState === SendState.CanSend) {
-            label = _t("Send");
-            className = "mx_ForwardList_canSend";
-        } else if (sendState === SendState.Sending) {
-            label = _t("Sending…");
-            className = "mx_ForwardList_sending";
-        } else if (sendState === SendState.Sent) {
-            label = _t("Sent");
-            className = "mx_ForwardList_sent";
+    let className;
+    let disabled = false;
+    let title;
+    let icon;
+    if (sendState === SendState.CanSend) {
+        className = "mx_ForwardList_canSend";
+        if (room.maySendMessage()) {
+            title = _t("Send");
         } else {
-            label = _t("Failed to send");
-            className = "mx_ForwardList_sendFailed";
+            disabled = true;
+            title = _t("You do not have permission to do this");
         }
-
-        button =
-            <AccessibleButton
-                kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
-                className={`mx_ForwardList_sendButton ${className}`}
-                onClick={send}
-                disabled={sendState !== SendState.CanSend}
-            >
-                { label }
-            </AccessibleButton>;
+    } else if (sendState === SendState.Sending) {
+        className = "mx_ForwardList_sending";
+        disabled = true;
+        title = _t("Sending…");
+        icon = <div className="mx_ForwardList_sendIcon"></div>;
+    } else if (sendState === SendState.Sent) {
+        className = "mx_ForwardList_sent";
+        disabled = true;
+        title = _t("Sent");
+        icon = <div className="mx_ForwardList_sendIcon"></div>;
     } else {
-        button =
-            <AccessibleTooltipButton
-                kind="primary_outline"
-                className="mx_ForwardList_sendButton mx_ForwardList_canSend"
-                onClick={() => {}}
-                disabled={true}
-                title={_t("You do not have permission to post to this room")}
-            >
-                { _t("Send") }
-            </AccessibleTooltipButton>;
+        className = "mx_ForwardList_sendFailed";
+        disabled = true;
+        title = _t("Failed to send");
+        icon = <NotificationBadge
+            notification={StaticNotificationState.RED_EXCLAMATION}
+        />;
     }
 
     return <div className="mx_ForwardList_entry">
-        <AccessibleButton className="mx_ForwardList_roomButton" onClick={jumpToRoom}>
+        <AccessibleTooltipButton
+            className="mx_ForwardList_roomButton"
+            onClick={jumpToRoom}
+            title={_t("Open link")}
+            yOffset={-20}
+            alignment={Alignment.Top}
+        >
             <DecoratedRoomAvatar room={room} avatarSize={32} />
             <span className="mx_ForwardList_entry_name">{ room.name }</span>
-        </AccessibleButton>
-        { button }
+        </AccessibleTooltipButton>
+        <AccessibleTooltipButton
+            kind={sendState === SendState.Failed ? "danger_outline" : "primary_outline"}
+            className={`mx_ForwardList_sendButton ${className}`}
+            onClick={send}
+            disabled={disabled}
+            title={title}
+            yOffset={-20}
+            alignment={Alignment.Top}
+        >
+            <div className="mx_ForwardList_sendLabel">{ _t("Send") }</div>
+            { icon }
+        </AccessibleTooltipButton>
     </div>;
 };
 
@@ -180,6 +190,7 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinis
         onFinished={onFinished}
         fixedWidth={false}
     >
+        <h3>{ _t("Message preview") }</h3>
         <div className={classnames("mx_ForwardDialog_preview", {
             "mx_IRCLayout": previewLayout == Layout.IRC,
             "mx_GroupLayout": previewLayout == Layout.Group,
@@ -191,11 +202,11 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinis
                 permalinkCreator={permalinkCreator}
             />
         </div>
+        <hr />
         <div className="mx_ForwardList">
-            <h2>{ _t("Forward to") }</h2>
             <SearchBox
                 className="mx_textinput_icon mx_textinput_search"
-                placeholder={ _t("Filter your rooms and DMs") }
+                placeholder={ _t("Search for rooms or people") }
                 onSearch={setQuery}
                 autoComplete={true}
                 autoFocus={true}
diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx
index 3bb264fb3e..95151d94a2 100644
--- a/src/components/views/elements/AccessibleTooltipButton.tsx
+++ b/src/components/views/elements/AccessibleTooltipButton.tsx
@@ -19,7 +19,7 @@ import React from 'react';
 import classNames from 'classnames';
 
 import AccessibleButton from "./AccessibleButton";
-import Tooltip from './Tooltip';
+import Tooltip, {Alignment} from './Tooltip';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
@@ -28,6 +28,7 @@ interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
     tooltipClassName?: string;
     forceHide?: boolean;
     yOffset?: number;
+    alignment?: Alignment;
 }
 
 interface IState {
@@ -66,13 +67,14 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
 
     render() {
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
-        const {title, tooltip, children, tooltipClassName, forceHide, yOffset, ...props} = this.props;
+        const {title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, ...props} = this.props;
 
         const tip = this.state.hover ? <Tooltip
             className="mx_AccessibleTooltipButton_container"
             tooltipClassName={classNames("mx_AccessibleTooltipButton_tooltip", tooltipClassName)}
             label={tooltip || title}
             yOffset={yOffset}
+            alignment={alignment}
         /> : <div />;
         return (
             <AccessibleButton
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 9ad93f5992..ebe5f54f61 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2202,11 +2202,13 @@
     "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
     "Report a bug": "Report a bug",
     "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
+    "You do not have permission to do this": "You do not have permission to do this",
     "Sending…": "Sending…",
     "Sent": "Sent",
+    "Open link": "Open link",
     "Forward message": "Forward message",
-    "Forward to": "Forward to",
-    "Filter your rooms and DMs": "Filter your rooms and DMs",
+    "Message preview": "Message preview",
+    "Search for rooms or people": "Search for rooms or people",
     "Confirm abort of host creation": "Confirm abort of host creation",
     "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
     "Abort": "Abort",
diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
index 331ee9d131..ce62b4fa7c 100644
--- a/test/components/views/dialogs/ForwardDialog-test.js
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -101,30 +101,30 @@ describe("ForwardDialog", () => {
         }));
 
         const firstButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
-        expect(firstButton.text()).toBe("Send");
+        expect(firstButton.render().is(".mx_ForwardList_canSend")).toBe(true);
 
         act(() => { firstButton.simulate("click"); });
-        expect(firstButton.text()).toBe("Sending…");
+        expect(firstButton.render().is(".mx_ForwardList_sending")).toBe(true);
 
         await act(async () => {
             cancelSend();
             // Wait one tick for the button to realize the send failed
             await new Promise(resolve => setImmediate(resolve));
         });
-        expect(firstButton.text()).toBe("Failed to send");
+        expect(firstButton.render().is(".mx_ForwardList_sendFailed")).toBe(true);
 
         const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").at(1);
-        expect(secondButton.render().text()).toBe("Send");
+        expect(secondButton.render().is(".mx_ForwardList_canSend")).toBe(true);
 
         act(() => { secondButton.simulate("click"); });
-        expect(secondButton.text()).toBe("Sending…");
+        expect(secondButton.render().is(".mx_ForwardList_sending")).toBe(true);
 
         await act(async () => {
             finishSend();
             // Wait one tick for the button to realize the send succeeded
             await new Promise(resolve => setImmediate(resolve));
         });
-        expect(secondButton.text()).toBe("Sent");
+        expect(secondButton.render().is(".mx_ForwardList_sent")).toBe(true);
     });
 
     it("can render replies", async () => {

From cd460a2555026fe0ceaf8bb53d31af7ad6405b64 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Fri, 21 May 2021 12:59:13 -0400
Subject: [PATCH 028/159] Adjust forward dialog copy

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/dialogs/ForwardDialog.tsx | 2 +-
 src/i18n/strings/en_EN.json                    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 3448e636d9..ff08ce43df 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -94,7 +94,7 @@ const Entry: React.FC<IEntryProps> = ({ room, event, cli, onFinished }) => {
             title = _t("Send");
         } else {
             disabled = true;
-            title = _t("You do not have permission to do this");
+            title = _t("You don't have permission to do this");
         }
     } else if (sendState === SendState.Sending) {
         className = "mx_ForwardList_sending";
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index ebe5f54f61..ceb48852c5 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2202,7 +2202,7 @@
     "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
     "Report a bug": "Report a bug",
     "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
-    "You do not have permission to do this": "You do not have permission to do this",
+    "You don't have permission to do this": "You don't have permission to do this",
     "Sending…": "Sending…",
     "Sent": "Sent",
     "Open link": "Open link",

From 400917623ce24f37b4eb12fda5769531f7c635c5 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 24 May 2021 08:33:28 -0400
Subject: [PATCH 029/159] Make myself the copyright holder for forward dialog
 code

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss           | 2 +-
 src/components/views/dialogs/ForwardDialog.tsx      | 2 +-
 test/components/views/dialogs/ForwardDialog-test.js | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index 1eb37e09da..593fe12531 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021 Robin Townsend.
 
 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/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index ff08ce43df..66b4b49997 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021 Robin Townsend.
 
 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/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
index ce62b4fa7c..fddba1d5ae 100644
--- a/test/components/views/dialogs/ForwardDialog-test.js
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021 Robin Townsend.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From 6ced61b7096ed31cd00ada688495235fa61b9df8 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 24 May 2021 08:34:03 -0400
Subject: [PATCH 030/159] Use camelCase

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/context_menus/MessageContextMenu.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index 5494b0a32f..4120f7be9e 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -161,7 +161,7 @@ export default class MessageContextMenu extends React.Component {
             cli: MatrixClientPeg.get(),
             event: this.props.mxEvent,
             permalinkCreator: this.props.permalinkCreator,
-        }, 'mx_Dialog_forwardmessage');
+        }, 'mx_Dialog_forwardMessage');
         this.closeMenu();
     };
 

From 121ed5eba996f41ce78c8b789278d196443e53b1 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 24 May 2021 08:51:04 -0400
Subject: [PATCH 031/159] Pass Matrix client around as matrixClient

Signed-off-by: Robin Townsend <robin@robin.town>
---
 .../views/context_menus/MessageContextMenu.js          |  2 +-
 src/components/views/dialogs/ForwardDialog.tsx         | 10 +++++-----
 test/components/views/dialogs/ForwardDialog-test.js    |  2 +-
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index 4120f7be9e..08fc5844ec 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -158,7 +158,7 @@ export default class MessageContextMenu extends React.Component {
 
     onForwardClick = () => {
         Modal.createTrackedDialog('Forward Message', '', ForwardDialog, {
-            cli: MatrixClientPeg.get(),
+            matrixClient: MatrixClientPeg.get(),
             event: this.props.mxEvent,
             permalinkCreator: this.props.permalinkCreator,
         }, 'mx_Dialog_forwardMessage');
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 66b4b49997..3087ce51a1 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -42,7 +42,7 @@ import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/Recent
 const AVATAR_SIZE = 30;
 
 interface IProps extends IDialogProps {
-    cli: MatrixClient;
+    matrixClient: MatrixClient;
     // The event to forward
     event: MatrixEvent;
     // We need a permalink creator for the source room to pass through to EventTile
@@ -53,7 +53,7 @@ interface IProps extends IDialogProps {
 interface IEntryProps {
     room: Room;
     event: MatrixEvent;
-    cli: MatrixClient;
+    matrixClient: MatrixClient;
     onFinished(success: boolean): void;
 }
 
@@ -64,7 +64,7 @@ enum SendState {
     Failed,
 }
 
-const Entry: React.FC<IEntryProps> = ({ room, event, cli, onFinished }) => {
+const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinished }) => {
     const [sendState, setSendState] = useState<SendState>(SendState.CanSend);
 
     const jumpToRoom = () => {
@@ -141,7 +141,7 @@ const Entry: React.FC<IEntryProps> = ({ room, event, cli, onFinished }) => {
     </div>;
 };
 
-const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinished }) => {
+const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
     const userId = cli.getUserId();
     const [profileInfo, setProfileInfo] = useState<any>({});
     useEffect(() => {
@@ -219,7 +219,7 @@ const ForwardDialog: React.FC<IProps> = ({ cli, event, permalinkCreator, onFinis
                                 key={room.roomId}
                                 room={room}
                                 event={event}
-                                cli={cli}
+                                matrixClient={cli}
                                 onFinished={onFinished}
                             />,
                         ) }
diff --git a/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
index fddba1d5ae..004954c713 100644
--- a/test/components/views/dialogs/ForwardDialog-test.js
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -47,7 +47,7 @@ describe("ForwardDialog", () => {
         await act(async () => {
             wrapper = mount(
                 <ForwardDialog
-                    cli={client}
+                    matrixClient={client}
                     event={message}
                     permalinkCreator={new RoomPermalinkCreator(undefined, sourceRoom)}
                     onFinished={jest.fn()}

From 5c7da97ff644036896cb113da21e69c2b71d2450 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 24 May 2021 08:55:08 -0400
Subject: [PATCH 032/159] Give forward dialog send buttons an accessible label

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/dialogs/ForwardDialog.tsx | 6 +++---
 src/i18n/strings/en_EN.json                    | 3 +--
 2 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 3087ce51a1..59475b1b51 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -99,13 +99,13 @@ const Entry: React.FC<IEntryProps> = ({ room, event, matrixClient: cli, onFinish
     } else if (sendState === SendState.Sending) {
         className = "mx_ForwardList_sending";
         disabled = true;
-        title = _t("Sending…");
-        icon = <div className="mx_ForwardList_sendIcon"></div>;
+        title = _t("Sending");
+        icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
     } else if (sendState === SendState.Sent) {
         className = "mx_ForwardList_sent";
         disabled = true;
         title = _t("Sent");
-        icon = <div className="mx_ForwardList_sendIcon"></div>;
+        icon = <div className="mx_ForwardList_sendIcon" aria-label={title}></div>;
     } else {
         className = "mx_ForwardList_sendFailed";
         disabled = true;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index c2c6aa9e6d..817765ca69 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2203,7 +2203,7 @@
     "Report a bug": "Report a bug",
     "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
     "You don't have permission to do this": "You don't have permission to do this",
-    "Sending…": "Sending…",
+    "Sending": "Sending",
     "Sent": "Sent",
     "Open link": "Open link",
     "Forward message": "Forward message",
@@ -2668,7 +2668,6 @@
     "Some of your messages have not been sent": "Some of your messages have not been sent",
     "Delete all": "Delete all",
     "Retry all": "Retry all",
-    "Sending": "Sending",
     "You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
     "Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
     "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",

From f75fb3b3499c8a886b81d5a84e86596c5659b918 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 27 May 2021 15:51:25 +0100
Subject: [PATCH 033/159] Add footer and privacy note to the start dm dialog

---
 res/css/views/dialogs/_InviteDialog.scss      | 61 ++++++++++++++++++-
 src/components/views/dialogs/InviteDialog.tsx | 58 ++++++++++++++++--
 src/i18n/strings/en_EN.json                   |  3 +
 3 files changed, 116 insertions(+), 6 deletions(-)

diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss
index d8ff56663a..a33871eca5 100644
--- a/res/css/views/dialogs/_InviteDialog.scss
+++ b/res/css/views/dialogs/_InviteDialog.scss
@@ -82,6 +82,14 @@ limitations under the License.
         text-transform: uppercase;
     }
 
+    > p {
+        margin: 0;
+    }
+
+    > span {
+        color: $primary-fg-color;
+    }
+
     .mx_InviteDialog_subname {
         margin-bottom: 10px;
         margin-top: -10px; // HACK: Positioning with margins is bad
@@ -90,6 +98,49 @@ limitations under the License.
     }
 }
 
+.mx_InviteDialog_footer {
+    border-top: 1px solid $input-border-color;
+
+    > h3 {
+        margin: 8px 0;
+        font-size: $font-12px;
+        color: $muted-fg-color;
+        font-weight: bold;
+        text-transform: uppercase;
+    }
+
+    .mx_InviteDialog_footer_link {
+        display: flex;
+        justify-content: space-between;
+        border-radius: 4px;
+        border: solid 1px $input-border-color;
+        padding: 8px;
+
+        > a {
+            text-decoration: none;
+            flex-shrink: 1;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+    }
+
+    .mx_InviteDialog_footer_link_copy {
+        flex-shrink: 0;
+        cursor: pointer;
+        margin-left: 20px;
+        display: inherit;
+
+        > div {
+            mask-image: url($copy-button-url);
+            background-color: $message-action-bar-fg-color;
+            margin-left: 5px;
+            width: 20px;
+            height: 20px;
+            background-repeat: no-repeat;
+        }
+    }
+}
+
 .mx_InviteDialog_roomTile {
     cursor: pointer;
     padding: 5px 10px;
@@ -212,15 +263,21 @@ limitations under the License.
 
 .mx_InviteDialog {
     // Prevent the dialog from jumping around randomly when elements change.
-    height: 590px;
+    height: 600px;
     padding-left: 20px; // the design wants some padding on the left
+    display: flex;
+    flex-direction: column;
+
+    .mx_InviteDialog_content {
+        overflow: hidden;
+    }
 }
 
 .mx_InviteDialog_userSections {
     margin-top: 10px;
     overflow-y: auto;
     padding-right: 45px;
-    height: 455px; // mx_InviteDialog's height minus some for the upper elements
+    height: calc(100% - 190px); // mx_InviteDialog's height minus some for the upper elements
 }
 
 // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index ec9c71ccbe..22763ceda2 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -47,6 +47,11 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 import {mediaFromMxc} from "../../../customisations/Media";
 import {getAddressType} from "../../../UserAddress";
+import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import {copyPlaintext, selectText} from "../../../utils/strings";
+import * as ContextMenu from "../../structures/ContextMenu";
+import {toRightOf} from "../../structures/ContextMenu";
+import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
 
 // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
 /* eslint-disable camelcase */
@@ -349,6 +354,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         initialText: "",
     };
 
+    private closeCopiedTooltip: () => void;
     _debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
     _editorRef: any = null;
 
@@ -400,6 +406,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         }
     }
 
+    componentWillUnmount() {
+        // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
+        // the tooltip otherwise, such as pressing Escape or clicking X really quickly
+        if (this.closeCopiedTooltip) this.closeCopiedTooltip();
+    }
+
     private onConsultFirstChange = (ev) => {
         this.setState({consultFirst: ev.target.checked});
     }
@@ -1232,6 +1244,25 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         }
     }
 
+    async onLinkClick(e) {
+        e.preventDefault();
+        selectText(e.target);
+    }
+
+    async onCopyClick(e) {
+        e.preventDefault();
+        const target = e.target; // copy target before we go async and React throws it away
+
+        const successful = await copyPlaintext(makeUserPermalink(MatrixClientPeg.get().getUserId()));
+        const buttonRect = target.getBoundingClientRect();
+        const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
+            ...toRightOf(buttonRect, 2),
+            message: successful ? _t("Copied!") : _t("Failed to copy"),
+        });
+        // Drop a reference to this close handler for componentWillUnmount
+        this.closeCopiedTooltip = target.onmouseleave = close;
+    }
+
     render() {
         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
         const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@@ -1242,12 +1273,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             spinner = <Spinner w={20} h={20} />;
         }
 
-
         let title;
         let helpText;
         let buttonText;
         let goButtonFn;
-        let consultSection;
+        let extraSection;
+        let footer;
         let keySharingWarning = <span />;
 
         const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
@@ -1310,6 +1341,24 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             }
             buttonText = _t("Go");
             goButtonFn = this._startDm;
+            extraSection = <div className="mx_InviteDialog_section">
+                <span>{ _t("Some results may be hidden for privacy.") }</span>
+                <p>{ _t("If you can’t see who you’re looking for, send them your invite link below.") }</p>
+            </div>;
+            const link = makeUserPermalink(MatrixClientPeg.get().getUserId());
+            footer = <div className="mx_InviteDialog_footer">
+                <h3>{ _t("Or send invite link") }</h3>
+                <div className="mx_InviteDialog_footer_link">
+                    <a href={link} onClick={this.onLinkClick}>
+                        { link }
+                    </a>
+                    <AccessibleTooltipButton
+                        title={_t("Copy")}
+                        onClick={this.onCopyClick}
+                        className="mx_InviteDialog_footer_link_copy"
+                    />
+                </div>
+            </div>
         } else if (this.props.kind === KIND_INVITE) {
             const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
             const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
@@ -1371,7 +1420,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             title = _t("Transfer");
             buttonText = _t("Transfer");
             goButtonFn = this._transferCall;
-            consultSection = <div>
+            footer = <div>
                 <label>
                     <input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
                     {_t("Consult first")}
@@ -1412,8 +1461,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                     <div className='mx_InviteDialog_userSections'>
                         {this._renderSection('recents')}
                         {this._renderSection('suggestions')}
+                        {extraSection}
                     </div>
-                    {consultSection}
+                    {footer}
                 </div>
             </BaseDialog>
         );
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 1b04ae3b89..9767d7ac76 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2246,6 +2246,9 @@
     "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",
+    "Some results may be hidden for privacy.": "Some results may be hidden for privacy.",
+    "If you can’t see who you’re looking for, send them your invite link below.": "If you can’t see who you’re looking for, send them your invite link below.",
+    "Or send invite link": "Or send invite link",
     "Unnamed Space": "Unnamed Space",
     "Invite to %(roomName)s": "Invite to %(roomName)s",
     "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 2c750fcb7a3bc7317b4b8b0e6013aa3f43e584a8 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 28 May 2021 12:48:12 +0100
Subject: [PATCH 034/159] Fix overflow issue in suggestion tiles

---
 res/css/views/dialogs/_InviteDialog.scss | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss
index a33871eca5..4016e7d2e3 100644
--- a/res/css/views/dialogs/_InviteDialog.scss
+++ b/res/css/views/dialogs/_InviteDialog.scss
@@ -193,6 +193,7 @@ limitations under the License.
 
     .mx_InviteDialog_roomTile_nameStack {
         display: inline-block;
+        overflow: hidden;
     }
 
     .mx_InviteDialog_roomTile_name {
@@ -208,6 +209,13 @@ limitations under the License.
         margin-left: 7px;
     }
 
+    .mx_InviteDialog_roomTile_name,
+    .mx_InviteDialog_roomTile_userId {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+    }
+
     .mx_InviteDialog_roomTile_time {
         text-align: right;
         font-size: $font-12px;

From ea263937095053bf10ded22c2e0a7ec3da8e7cb5 Mon Sep 17 00:00:00 2001
From: Nique Woodhouse <nique.shjm.woodhouse@gmail.com>
Date: Fri, 28 May 2021 13:00:18 +0100
Subject: [PATCH 035/159] Styling amends to accommodate the invite dialog
 footer

---
 res/css/views/dialogs/_InviteDialog.scss      | 34 ++++++++++++++-----
 src/components/views/dialogs/InviteDialog.tsx |  4 +--
 src/i18n/strings/en_EN.json                   |  2 +-
 3 files changed, 29 insertions(+), 11 deletions(-)

diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss
index a33871eca5..bda576c44e 100644
--- a/res/css/views/dialogs/_InviteDialog.scss
+++ b/res/css/views/dialogs/_InviteDialog.scss
@@ -73,7 +73,7 @@ limitations under the License.
 }
 
 .mx_InviteDialog_section {
-    padding-bottom: 10px;
+    padding-bottom: 4px;
 
     h3 {
         font-size: $font-12px;
@@ -98,11 +98,25 @@ limitations under the License.
     }
 }
 
+.mx_InviteDialog_section_hidden_suggestions_disclaimer {
+    padding: 8px 0 16px 0;
+    font-size: $font-14px;
+
+    > span {
+        color: $primary-fg-color;
+        font-weight: 600;
+    }
+
+    > p {
+        margin:0;
+    }
+}
+
 .mx_InviteDialog_footer {
     border-top: 1px solid $input-border-color;
 
     > h3 {
-        margin: 8px 0;
+        margin: 12px 0;
         font-size: $font-12px;
         color: $muted-fg-color;
         font-weight: bold;
@@ -113,7 +127,7 @@ limitations under the License.
         display: flex;
         justify-content: space-between;
         border-radius: 4px;
-        border: solid 1px $input-border-color;
+        border: solid 1px $light-fg-color;  
         padding: 8px;
 
         > a {
@@ -274,17 +288,21 @@ limitations under the License.
 }
 
 .mx_InviteDialog_userSections {
-    margin-top: 10px;
+    margin-top: 4px;
     overflow-y: auto;
-    padding-right: 45px;
-    height: calc(100% - 190px); // mx_InviteDialog's height minus some for the upper elements
+    padding: 0px 45px 4px 0px;
+    height: calc(100% - 175px); // mx_InviteDialog's height minus some for the upper  and lower elements
 }
 
+
 // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
 // for the user section gets weird.
-.mx_InviteDialog_helpText,
 .mx_InviteDialog_addressBar {
-    margin-right: 45px;
+    margin: 8px 45px 0px 0px;
+}
+
+.mx_InviteDialog_helpText {
+    margin:0px;
 }
 
 .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 22763ceda2..081004fa74 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -1341,8 +1341,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             }
             buttonText = _t("Go");
             goButtonFn = this._startDm;
-            extraSection = <div className="mx_InviteDialog_section">
-                <span>{ _t("Some results may be hidden for privacy.") }</span>
+            extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
+                <span>{ _t("Some suggestions may be hidden for privacy.") }</span>
                 <p>{ _t("If you can’t see who you’re looking for, send them your invite link below.") }</p>
             </div>;
             const link = makeUserPermalink(MatrixClientPeg.get().getUserId());
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 9767d7ac76..8f5082e88a 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2246,7 +2246,7 @@
     "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",
-    "Some results may be hidden for privacy.": "Some results may be hidden for privacy.",
+    "Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.",
     "If you can’t see who you’re looking for, send them your invite link below.": "If you can’t see who you’re looking for, send them your invite link below.",
     "Or send invite link": "Or send invite link",
     "Unnamed Space": "Unnamed Space",

From 36e43270ca6acd79ad5244ec83b96344fd766592 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 28 May 2021 13:08:05 +0100
Subject: [PATCH 036/159] Apply suggestions from code review

---
 res/css/views/dialogs/_InviteDialog.scss | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss
index 0d78589db2..bae086c7d5 100644
--- a/res/css/views/dialogs/_InviteDialog.scss
+++ b/res/css/views/dialogs/_InviteDialog.scss
@@ -108,7 +108,7 @@ limitations under the License.
     }
 
     > p {
-        margin:0;
+        margin: 0;
     }
 }
 
@@ -298,7 +298,7 @@ limitations under the License.
 .mx_InviteDialog_userSections {
     margin-top: 4px;
     overflow-y: auto;
-    padding: 0px 45px 4px 0px;
+    padding: 0 45px 4px 0;
     height: calc(100% - 175px); // mx_InviteDialog's height minus some for the upper  and lower elements
 }
 
@@ -306,11 +306,11 @@ limitations under the License.
 // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
 // for the user section gets weird.
 .mx_InviteDialog_addressBar {
-    margin: 8px 45px 0px 0px;
+    margin: 8px 45px 0 0;
 }
 
 .mx_InviteDialog_helpText {
-    margin:0px;
+    margin: 0;
 }
 
 .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {

From caaef630776a07c69654393a37bd62bb56398566 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 28 May 2021 13:11:48 +0100
Subject: [PATCH 037/159] delint1

---
 res/css/views/dialogs/_InviteDialog.scss | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss
index bae086c7d5..8c0421b989 100644
--- a/res/css/views/dialogs/_InviteDialog.scss
+++ b/res/css/views/dialogs/_InviteDialog.scss
@@ -17,6 +17,9 @@ limitations under the License.
 .mx_InviteDialog_addressBar {
     display: flex;
     flex-direction: row;
+    // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
+    // for the user section gets weird.
+    margin: 8px 45px 0 0;
 
     .mx_InviteDialog_editor {
         flex: 1;
@@ -127,7 +130,7 @@ limitations under the License.
         display: flex;
         justify-content: space-between;
         border-radius: 4px;
-        border: solid 1px $light-fg-color;  
+        border: solid 1px $light-fg-color;
         padding: 8px;
 
         > a {
@@ -302,13 +305,6 @@ limitations under the License.
     height: calc(100% - 175px); // mx_InviteDialog's height minus some for the upper  and lower elements
 }
 
-
-// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
-// for the user section gets weird.
-.mx_InviteDialog_addressBar {
-    margin: 8px 45px 0 0;
-}
-
 .mx_InviteDialog_helpText {
     margin: 0;
 }

From 91b7f2551312c405257421221b4d237a3bd96682 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 28 May 2021 13:51:54 +0100
Subject: [PATCH 038/159] delint2

---
 src/components/views/dialogs/InviteDialog.tsx | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 7cee61d579..ef7a31a177 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -408,9 +408,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 
     componentWillUnmount() {
         this.unmounted = true;
-    }
-
-    componentWillUnmount() {
         // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
         // the tooltip otherwise, such as pressing Escape or clicking X really quickly
         if (this.closeCopiedTooltip) this.closeCopiedTooltip();

From 53ebf3b8e31560ee71d744d203f653b3dd36b32c Mon Sep 17 00:00:00 2001
From: Aaron Raimist <aaron@raim.ist>
Date: Sat, 29 May 2021 01:37:19 -0500
Subject: [PATCH 039/159] Don't include via when sharing room alias

Signed-off-by: Aaron Raimist <aaron@raim.ist>
---
 src/utils/permalinks/Permalinks.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts
index d87c826cc2..cecd79dd53 100644
--- a/src/utils/permalinks/Permalinks.ts
+++ b/src/utils/permalinks/Permalinks.ts
@@ -149,7 +149,7 @@ export class RoomPermalinkCreator {
             // Prefer to use canonical alias for permalink if possible
             const alias = this.room.getCanonicalAlias();
             if (alias) {
-                return getPermalinkConstructor().forRoom(alias, this._serverCandidates);
+                return getPermalinkConstructor().forRoom(alias);
             }
         }
         return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates);

From ccdd2311f447111f4cf56725834ba08be78abc08 Mon Sep 17 00:00:00 2001
From: Aaron Raimist <aaron@raim.ist>
Date: Sat, 29 May 2021 01:38:41 -0500
Subject: [PATCH 040/159] Make "share this room" use aliases if possible

Signed-off-by: Aaron Raimist <aaron@raim.ist>
---
 src/utils/permalinks/Permalinks.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts
index cecd79dd53..2f268aaa0c 100644
--- a/src/utils/permalinks/Permalinks.ts
+++ b/src/utils/permalinks/Permalinks.ts
@@ -293,16 +293,16 @@ export function makeRoomPermalink(roomId: string): string {
 
     // If the roomId isn't actually a room ID, don't try to list the servers.
     // Aliases are already routable, and don't need extra information.
-    if (roomId[0] !== '!') return getPermalinkConstructor().forRoom(roomId, []);
+    if (roomId[0] !== '!') return getPermalinkConstructor().forShareableRoom(roomId, []);
 
     const client = MatrixClientPeg.get();
     const room = client.getRoom(roomId);
     if (!room) {
-        return getPermalinkConstructor().forRoom(roomId, []);
+        return getPermalinkConstructor().forShareableRoom(roomId, []);
     }
     const permalinkCreator = new RoomPermalinkCreator(room);
     permalinkCreator.load();
-    return permalinkCreator.forRoom();
+    return permalinkCreator.forShareableRoom();
 }
 
 export function makeGroupPermalink(groupId: string): string {

From b032422c6a536b856d7d105dd593a804b245c933 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 1 Jun 2021 17:37:31 -0400
Subject: [PATCH 041/159] Fix whitespace lints

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/dialogs/ForwardDialog.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 59475b1b51..58759d7d42 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -184,13 +184,13 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
     const previewLayout = SettingsStore.getValue("layout");
 
     return <BaseDialog
-        title={ _t("Forward message") }
+        title={_t("Forward message")}
         className="mx_ForwardDialog"
         contentId="mx_ForwardList"
         onFinished={onFinished}
         fixedWidth={false}
     >
-        <h3>{ _t("Message preview") }</h3>
+        <h3>{_t("Message preview")}</h3>
         <div className={classnames("mx_ForwardDialog_preview", {
             "mx_IRCLayout": previewLayout == Layout.IRC,
             "mx_GroupLayout": previewLayout == Layout.Group,
@@ -206,7 +206,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
         <div className="mx_ForwardList">
             <SearchBox
                 className="mx_textinput_icon mx_textinput_search"
-                placeholder={ _t("Search for rooms or people") }
+                placeholder={_t("Search for rooms or people")}
                 onSearch={setQuery}
                 autoComplete={true}
                 autoFocus={true}

From 8efbdd04068092a8627a8827dfd98c2cd96de5cf Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 1 Jun 2021 17:56:46 -0400
Subject: [PATCH 042/159] Match forward dialog send failed indicator color with
 button

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index 593fe12531..fc59259ca2 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -124,7 +124,8 @@ limitations under the License.
                     }
 
                     .mx_NotificationBadge {
-                        opacity: 0.4;
+                        // Match the failed to send indicator's color with the disabled button
+                        background-color: $button-danger-disabled-fg-color;
                     }
 
                     &.mx_ForwardList_sending .mx_ForwardList_sendIcon {

From c78167977a49421156f62d995cf5aab97801a758 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 1 Jun 2021 17:57:26 -0400
Subject: [PATCH 043/159] Remove unused class

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/context_menus/MessageContextMenu.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index 08fc5844ec..442470bb52 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -161,7 +161,7 @@ export default class MessageContextMenu extends React.Component {
             matrixClient: MatrixClientPeg.get(),
             event: this.props.mxEvent,
             permalinkCreator: this.props.permalinkCreator,
-        }, 'mx_Dialog_forwardMessage');
+        });
         this.closeMenu();
     };
 

From 4ef69fcbf6cd5bb90261759eff4cd3e720a96f14 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 1 Jun 2021 18:09:51 -0400
Subject: [PATCH 044/159] Use settings hooks in forward dialog

...to dynamically watch for layout changes.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/dialogs/ForwardDialog.tsx | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 58759d7d42..2c4920da4b 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -22,7 +22,7 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
 
 import {_t} from "../../../languageHandler";
 import dis from "../../../dispatcher/dispatcher";
-import SettingsStore from "../../../settings/SettingsStore";
+import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings";
 import {UIFeature} from "../../../settings/UIFeature";
 import {Layout} from "../../../settings/Layout";
 import {IDialogProps} from "./IDialogProps";
@@ -174,14 +174,16 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
     const [query, setQuery] = useState("");
     const lcQuery = query.toLowerCase();
 
+    const spacesEnabled = useFeatureEnabled("feature_spaces");
+    const flairEnabled = useFeatureEnabled(UIFeature.Flair);
+    const previewLayout = useSettingValue("layout");
+
     const rooms = useMemo(() => sortRooms(
         cli.getVisibleRooms().filter(
             room => room.getMyMembership() === "join" &&
-                !(SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()),
+                !(spacesEnabled && room.isSpaceRoom()),
         ),
-    ), [cli]).filter(room => room.name.toLowerCase().includes(lcQuery));
-
-    const previewLayout = SettingsStore.getValue("layout");
+    ), [cli, spacesEnabled]).filter(room => room.name.toLowerCase().includes(lcQuery));
 
     return <BaseDialog
         title={_t("Forward message")}
@@ -198,7 +200,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
             <EventTile
                 mxEvent={mockEvent}
                 layout={previewLayout}
-                enableFlair={SettingsStore.getValue(UIFeature.Flair)}
+                enableFlair={flairEnabled}
                 permalinkCreator={permalinkCreator}
             />
         </div>

From 59660df0cbe74dbe39ada90e4296d427e5375bbc Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 1 Jun 2021 20:17:20 -0400
Subject: [PATCH 045/159] Use a QueryMatcher for forward dialog filtering

This also allows us to filter by room aliases.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/dialogs/ForwardDialog.tsx | 15 ++++++++++++---
 src/hooks/useSettings.ts                       |  4 ++--
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 2c4920da4b..cba292ea25 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -38,6 +38,7 @@ import {StaticNotificationState} from "../../../stores/notifications/StaticNotif
 import NotificationBadge from "../rooms/NotificationBadge";
 import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
 import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
+import QueryMatcher from "../../../autocomplete/QueryMatcher";
 
 const AVATAR_SIZE = 30;
 
@@ -176,14 +177,22 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
 
     const spacesEnabled = useFeatureEnabled("feature_spaces");
     const flairEnabled = useFeatureEnabled(UIFeature.Flair);
-    const previewLayout = useSettingValue("layout");
+    const previewLayout = useSettingValue<Layout>("layout");
 
-    const rooms = useMemo(() => sortRooms(
+    let rooms = useMemo(() => sortRooms(
         cli.getVisibleRooms().filter(
             room => room.getMyMembership() === "join" &&
                 !(spacesEnabled && room.isSpaceRoom()),
         ),
-    ), [cli, spacesEnabled]).filter(room => room.name.toLowerCase().includes(lcQuery));
+    ), [cli, spacesEnabled]);
+
+    if (lcQuery) {
+        rooms = new QueryMatcher<Room>(rooms, {
+            keys: ["name"],
+            funcs: [r => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
+            shouldMatchWordsOnly: false,
+        }).match(lcQuery);
+    }
 
     return <BaseDialog
         title={_t("Forward message")}
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index 3aa38c44f4..756c2d5868 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -35,8 +35,8 @@ export const useSettingValue = <T>(settingName: string, roomId: string = null, e
 };
 
 // Hook to fetch whether a feature is enabled and dynamically update when that changes
-export const useFeatureEnabled = (featureName: string, roomId: string = null) => {
-    const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId));
+export const useFeatureEnabled = (featureName: string, roomId: string = null): boolean => {
+    const [enabled, setEnabled] = useState(SettingsStore.getValue<boolean>(featureName, roomId));
 
     useEffect(() => {
         const ref = SettingsStore.watchSetting(featureName, roomId, () => {

From 992861a1cd821c2d7564fd644b4294220f54ff96 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 1 Jun 2021 20:36:28 -0400
Subject: [PATCH 046/159] Fix forward dialog tests

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

diff --git a/test/test-utils.js b/test/test-utils.js
index c82885b5f0..6053924103 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -265,6 +265,8 @@ export function mkStubRoom(roomId = null, name) {
         isSpaceRoom: jest.fn(() => false),
         getUnreadNotificationCount: jest.fn(() => 0),
         getEventReadUpTo: jest.fn(() => null),
+        getCanonicalAlias: jest.fn(),
+        getAltAliases: jest.fn().mockReturnValue([]),
         timeline: [],
     };
 }

From c1a763fb4f866268bc70c8ef44905cc75563703d Mon Sep 17 00:00:00 2001
From: zopieux <web@zopieux.com>
Date: Wed, 2 Jun 2021 23:47:21 +0000
Subject: [PATCH 047/159] Translated using Weblate (French)

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

diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index 5a8208f50b..ff48785427 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -2868,7 +2868,7 @@
     "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> 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",
+    "See <b>%(eventType)s</b> events posted to this room": "Voir les événements <b>%(eventType)s</b> envoyés dans ce salon",
     "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",
@@ -2930,7 +2930,7 @@
     "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> sous votre nom dans ce salon",
+    "Send <b>%(msgtype)s</b> messages as you in this room": "Envoie les 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",

From b4f8c66f7b2645a83b660478b006b7d0414f4b00 Mon Sep 17 00:00:00 2001
From: Miquel Lionel <lionel@les-miquelots.net>
Date: Wed, 2 Jun 2021 23:37:33 +0000
Subject: [PATCH 048/159] Translated using Weblate (French)

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

diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index ff48785427..e94dbd6c1f 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -2842,7 +2842,7 @@
     "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 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",
+    "Remain on your screen when viewing another room, when running": "Reste sur votre écran lors de l'appel quand vous regardez un autre 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",
@@ -2861,7 +2861,7 @@
     "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 your active room": "Voir les messages envoyés dans le salon actuel",
     "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 sous votre nom dans votre salon actif",
     "Send messages as you in this room": "Envoie des messages sous votre nom dans ce salon",
@@ -3034,7 +3034,7 @@
     "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",
+    "Change which room you're viewing": "Changer le salon que vous êtes en train de lire",
     "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 la conversation privée en salon",

From 9f1e1c24205fdc76125f7f1e4351b939b6b7d7b6 Mon Sep 17 00:00:00 2001
From: c-cal <github-2c7c@zebrina.fr>
Date: Tue, 1 Jun 2021 15:37:08 +0000
Subject: [PATCH 049/159] Translated using Weblate (French)

Currently translated at 100.0% (2979 of 2979 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 | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index e94dbd6c1f..7f3614bf90 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -1873,7 +1873,7 @@
     "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",
-    "Mod": "Modo",
+    "Mod": "Modérateur",
     "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.",
     "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Si vos autres sessions n’ont pas la clé pour ce message vous ne pourrez pas le déchiffrer.",
@@ -3111,7 +3111,7 @@
     "No permissions": "Aucune permission",
     "Remove from Space": "Supprimer de l’espace",
     "Undo": "Annuler",
-    "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.": "Votre message n’a pas été envoyé car ce serveur d’accueil a été banni par son administrateur. Merci de <a>contacter votre administrateur de service</a> pour poursuivre l’usage de ce 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.": "Votre message n’a pas été envoyé car ce serveur d’accueil a été bloqué par son administrateur. Merci de <a>contacter votre administrateur de service</a> pour continuer à utiliser le service.",
     "Are you sure you want to leave the space '%(spaceName)s'?": "Êtes-vous sûr de vouloir quitter l’espace « %(spaceName)s » ?",
     "This space is not public. You will not be able to rejoin without an invite.": "Cet espace n’est pas public. Vous ne pourrez pas le rejoindre sans invitation.",
     "Start audio stream": "Démarrer une diffusion audio",
@@ -3352,5 +3352,11 @@
     "sends space invaders": "Envoie les Space Invaders",
     "Sends the given message with a space themed effect": "Envoyer le message avec un effet lié au thème de l’espace",
     "See when people join, leave, or are invited to your active room": "Afficher quand des personnes rejoignent, partent, ou sont invités dans votre salon actif",
-    "Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir"
+    "Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir",
+    "Currently joining %(count)s rooms|one": "Vous êtes en train de rejoindre %(count)s salon",
+    "Currently joining %(count)s rooms|other": "Vous êtes en train de rejoindre %(count)s salons",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Essayez d'autres mots ou vérifiez les fautes de frappe. Certains salons peuvent ne pas être visibles car ils sont privés et vous devez être invité pour les rejoindre.",
+    "No results for \"%(query)s\"": "Aucun résultat pour « %(query)s »",
+    "The user you called is busy.": "L’utilisateur que vous avez appelé est indisponible.",
+    "User Busy": "Utilisateur indisponible"
 }

From 0c97d90fb98f08204c1d7853e0c0f6c30820e5a3 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Thu, 3 Jun 2021 16:44:28 +0100
Subject: [PATCH 050/159] Iterate PR based on feedback

---
 res/css/views/dialogs/_InviteDialog.scss      |  6 +++++-
 src/components/views/dialogs/InviteDialog.tsx | 16 +++++++++++-----
 2 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss
index 8c0421b989..2e48b5d8e9 100644
--- a/res/css/views/dialogs/_InviteDialog.scss
+++ b/res/css/views/dialogs/_InviteDialog.scss
@@ -302,7 +302,11 @@ limitations under the License.
     margin-top: 4px;
     overflow-y: auto;
     padding: 0 45px 4px 0;
-    height: calc(100% - 175px); // mx_InviteDialog's height minus some for the upper  and lower elements
+    height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements
+}
+
+.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections {
+    height: calc(100% - 175px);
 }
 
 .mx_InviteDialog_helpText {
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 5cbcb12c4f..557ea416a8 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {createRef} from 'react';
+import React, { createRef } from 'react';
+import classNames from 'classnames';
+
 import {_t, _td} from "../../../languageHandler";
 import * as sdk from "../../../index";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
@@ -1252,7 +1254,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         selectText(e.target);
     }
 
-    async onCopyClick(e) {
+    onCopyClick = async e => {
         e.preventDefault();
         const target = e.target; // copy target before we go async and React throws it away
 
@@ -1264,7 +1266,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         });
         // Drop a reference to this close handler for componentWillUnmount
         this.closeCopiedTooltip = target.onmouseleave = close;
-    }
+    };
 
     render() {
         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@@ -1359,7 +1361,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                         title={_t("Copy")}
                         onClick={this.onCopyClick}
                         className="mx_InviteDialog_footer_link_copy"
-                    />
+                    >
+                        <div />
+                    </AccessibleTooltipButton>
                 </div>
             </div>
         } else if (this.props.kind === KIND_INVITE) {
@@ -1437,7 +1441,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             || (this.state.filterText && this.state.filterText.includes('@'));
         return (
             <BaseDialog
-                className='mx_InviteDialog'
+                className={classNames("mx_InviteDialog", {
+                    mx_InviteDialog_hasFooter: !!footer,
+                })}
                 hasCancel={true}
                 onFinished={this.props.onFinished}
                 title={title}

From 48bd6583ed3e1431a995074dcb50ca47146f61a0 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 4 Jun 2021 11:34:54 +0100
Subject: [PATCH 051/159] Fix unpinning of pinned messages

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

diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx
index a3f1f2d9df..55809ee02b 100644
--- a/src/components/views/right_panel/PinnedMessagesCard.tsx
+++ b/src/components/views/right_panel/PinnedMessagesCard.tsx
@@ -155,7 +155,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
 
         // show them in reverse, with latest pinned at the top
         content = pinnedEvents.filter(Boolean).reverse().map(ev => (
-            <PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={onUnpinClicked} />
+            <PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={() => onUnpinClicked(ev)} />
         ));
     } else {
         content = <div className="mx_RightPanel_empty mx_PinnedMessagesCard_empty">

From 93f41ce0ab484172a76f0e07e4036eace2fe9bce Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 4 Jun 2021 11:35:17 +0100
Subject: [PATCH 052/159] Actually finish the empty state for the pinned
 messages card

---
 .../right_panel/_PinnedMessagesCard.scss      | 55 +++++++++++++++++++
 .../views/right_panel/PinnedMessagesCard.tsx  | 17 +++++-
 src/i18n/strings/en_EN.json                   |  6 +-
 3 files changed, 73 insertions(+), 5 deletions(-)

diff --git a/res/css/views/right_panel/_PinnedMessagesCard.scss b/res/css/views/right_panel/_PinnedMessagesCard.scss
index b6b8238bed..785aee09ca 100644
--- a/res/css/views/right_panel/_PinnedMessagesCard.scss
+++ b/res/css/views/right_panel/_PinnedMessagesCard.scss
@@ -32,4 +32,59 @@ limitations under the License.
             margin-right: 6px;
         }
     }
+
+    .mx_PinnedMessagesCard_empty {
+        display: flex;
+        height: 100%;
+
+        > div {
+            height: max-content;
+            text-align: center;
+            margin: auto 40px;
+
+            .mx_PinnedMessagesCard_MessageActionBar {
+                pointer-events: none;
+                display: flex;
+                height: 32px;
+                line-height: $font-24px;
+                border-radius: 8px;
+                background: $primary-bg-color;
+                border: 1px solid $input-border-color;
+                padding: 1px;
+                width: max-content;
+                margin: 0 auto;
+                box-sizing: border-box;
+
+                .mx_MessageActionBar_maskButton {
+                    display: inline-block;
+                    position: relative;
+                }
+
+                .mx_MessageActionBar_optionsButton {
+                    background: $roomlist-button-bg-color;
+                    border-radius: 6px;
+                    z-index: 1;
+
+                    &::after {
+                        background-color: $primary-fg-color;
+                    }
+                }
+            }
+
+            > h2 {
+                font-weight: $font-semi-bold;
+                font-size: $font-15px;
+                line-height: $font-24px;
+                color: $primary-fg-color;
+                margin-top: 24px;
+                margin-bottom: 20px;
+            }
+
+            > span {
+                font-size: $font-12px;
+                line-height: $font-15px;
+                color: $secondary-fg-color;
+            }
+        }
+    }
 }
diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx
index 55809ee02b..3a4dc9cc92 100644
--- a/src/components/views/right_panel/PinnedMessagesCard.tsx
+++ b/src/components/views/right_panel/PinnedMessagesCard.tsx
@@ -158,9 +158,20 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
             <PinnedEventTile key={ev.getId()} room={room} event={ev} onUnpinClicked={() => onUnpinClicked(ev)} />
         ));
     } else {
-        content = <div className="mx_RightPanel_empty mx_PinnedMessagesCard_empty">
-            <h2>{_t("You’re all caught up")}</h2>
-            <p>{_t("You have no visible notifications.")}</p>
+        content = <div className="mx_PinnedMessagesCard_empty">
+            <div>
+                <div className="mx_PinnedMessagesCard_MessageActionBar">
+                    <div className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
+                    <div className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" />
+                    <div className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton" />
+                </div>
+
+                <h2>{ _t("Nothing pinned, yet") }</h2>
+                { _t("If you have permissions, open the menu on any message and select " +
+                    "<b>Pin</b> to stick them here.", {}, {
+                        b: sub => <b>{ sub }</b>,
+                }) }
+            </div>
         </div>;
     }
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 9e85ea28c8..302c8709fa 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1717,8 +1717,8 @@
     "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",
-    "You’re all caught up": "You’re all caught up",
-    "You have no visible notifications.": "You have no visible notifications.",
+    "Nothing pinned, yet": "Nothing pinned, yet",
+    "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
     "Pinned messages": "Pinned messages",
     "Room Info": "Room Info",
     "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
@@ -2628,6 +2628,8 @@
     "Create a new community": "Create a new community",
     "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
     "Communities are changing to Spaces": "Communities are changing to Spaces",
+    "You’re all caught up": "You’re all caught up",
+    "You have no visible notifications.": "You have no visible notifications.",
     "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
     "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.",
     "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.",

From 48d3e41351bd869c232176d956d8254d17f5dc77 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sat, 5 Jun 2021 01:23:51 -0400
Subject: [PATCH 053/159] Cache frequently used settings values in RoomContext

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/structures/RoomView.tsx | 64 +++++++++++++++++++++-----
 src/contexts/RoomContext.ts            |  6 ++-
 2 files changed, 57 insertions(+), 13 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 5ffc2fd0da..fa67013ccb 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -155,7 +155,6 @@ export interface IState {
     canPeek: boolean;
     showApps: boolean;
     isPeeking: boolean;
-    showReadReceipts: boolean;
     showRightPanel: boolean;
     // error object, as from the matrix client/server API
     // If we failed to load information about the room,
@@ -183,6 +182,11 @@ export interface IState {
     canReact: boolean;
     canReply: boolean;
     layout: Layout;
+    showReadReceipts: boolean;
+    showRedactions: boolean;
+    showJoinLeaves: boolean;
+    showAvatarChanges: boolean;
+    showDisplaynameChanges: boolean;
     matrixClientIsReady: boolean;
     showUrlPreview?: boolean;
     e2eStatus?: E2EStatus;
@@ -200,8 +204,7 @@ export default class RoomView extends React.Component<IProps, IState> {
     private readonly dispatcherRef: string;
     private readonly roomStoreToken: EventSubscription;
     private readonly rightPanelStoreToken: EventSubscription;
-    private readonly showReadReceiptsWatchRef: string;
-    private readonly layoutWatcherRef: string;
+    private settingWatchers: string[];
 
     private unmounted = false;
     private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
@@ -232,7 +235,6 @@ export default class RoomView extends React.Component<IProps, IState> {
             canPeek: false,
             showApps: false,
             isPeeking: false,
-            showReadReceipts: true,
             showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
             joining: false,
             atEndOfLiveTimeline: true,
@@ -242,6 +244,11 @@ export default class RoomView extends React.Component<IProps, IState> {
             canReact: false,
             canReply: false,
             layout: SettingsStore.getValue("layout"),
+            showReadReceipts: true,
+            showRedactions: true,
+            showJoinLeaves: true,
+            showAvatarChanges: true,
+            showDisplaynameChanges: true,
             matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
             dragCounter: 0,
         };
@@ -268,9 +275,11 @@ export default class RoomView extends React.Component<IProps, IState> {
         WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
         WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
 
-        this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
-            this.onReadReceiptsChange);
-        this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
+        this.settingWatchers = [
+            SettingsStore.watchSetting("layout", null, () =>
+                this.setState({ layout: SettingsStore.getValue("layout") }),
+            ),
+        ];
     }
 
     private onWidgetStoreUpdate = () => {
@@ -327,9 +336,42 @@ export default class RoomView extends React.Component<IProps, IState> {
             // we should only peek once we have a ready client
             shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
             showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
+            showRedactions: SettingsStore.getValue("showRedactions", roomId),
+            showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
+            showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
+            showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
             wasContextSwitch: RoomViewStore.getWasContextSwitch(),
         };
 
+        // Add watchers for each of the settings we just looked up
+        this.settingWatchers = this.settingWatchers.concat([
+            SettingsStore.watchSetting("showReadReceipts", null, () =>
+                this.setState({
+                    showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
+                }),
+            ),
+            SettingsStore.watchSetting("showRedactions", null, () =>
+                this.setState({
+                    showRedactions: SettingsStore.getValue("showRedactions", roomId),
+                }),
+            ),
+            SettingsStore.watchSetting("showJoinLeaves", null, () =>
+                this.setState({
+                    showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
+                }),
+            ),
+            SettingsStore.watchSetting("showAvatarChanges", null, () =>
+                this.setState({
+                    showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
+                }),
+            ),
+            SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
+                this.setState({
+                    showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
+                }),
+            ),
+        ]);
+
         if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
             // Stop peeking because we have joined this room now
             this.context.stopPeeking();
@@ -638,10 +680,6 @@ export default class RoomView extends React.Component<IProps, IState> {
             );
         }
 
-        if (this.showReadReceiptsWatchRef) {
-            SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
-        }
-
         // cancel any pending calls to the rate_limited_funcs
         this.updateRoomMembers.cancelPendingCall();
 
@@ -649,7 +687,9 @@ export default class RoomView extends React.Component<IProps, IState> {
         // console.log("Tinter.tint from RoomView.unmount");
         // Tinter.tint(); // reset colourscheme
 
-        SettingsStore.unwatchSetting(this.layoutWatcherRef);
+        for (const watcher of this.settingWatchers) {
+            SettingsStore.unwatchSetting(watcher);
+        }
     }
 
     private onUserScroll = () => {
diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts
index e925f8624b..1efa1c03e7 100644
--- a/src/contexts/RoomContext.ts
+++ b/src/contexts/RoomContext.ts
@@ -31,7 +31,6 @@ const RoomContext = createContext<IState>({
     canPeek: false,
     showApps: false,
     isPeeking: false,
-    showReadReceipts: true,
     showRightPanel: true,
     joining: false,
     atEndOfLiveTimeline: true,
@@ -41,6 +40,11 @@ const RoomContext = createContext<IState>({
     canReact: false,
     canReply: false,
     layout: Layout.Group,
+    showReadReceipts: true,
+    showRedactions: true,
+    showJoinLeaves: true,
+    showAvatarChanges: true,
+    showDisplaynameChanges: true,
     matrixClientIsReady: false,
     dragCounter: 0,
 });

From 13196b8146919f8ef781904d02c1edfd5f376dfe Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sat, 5 Jun 2021 01:25:01 -0400
Subject: [PATCH 054/159] Prefer cached settings values in shouldHideEvent

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/shouldHideEvent.ts | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/src/shouldHideEvent.ts b/src/shouldHideEvent.ts
index 2a47b9c417..31d610b28b 100644
--- a/src/shouldHideEvent.ts
+++ b/src/shouldHideEvent.ts
@@ -17,6 +17,7 @@
 import {MatrixEvent} from "matrix-js-sdk/src/models/event";
 
 import SettingsStore from "./settings/SettingsStore";
+import {IState} from "./components/structures/RoomView";
 
 interface IDiff {
     isMemberEvent: boolean;
@@ -47,11 +48,18 @@ function memberEventDiff(ev: MatrixEvent): IDiff {
     return diff;
 }
 
-export default function shouldHideEvent(ev: MatrixEvent): boolean {
-    // Wrap getValue() for readability. Calling the SettingsStore can be
-    // fairly resource heavy, so the checks below should avoid hitting it
-    // where possible.
-    const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId());
+/**
+ * Determines whether the given event should be hidden from timelines.
+ * @param ev The event
+ * @param ctx An optional RoomContext to pull cached settings values from to avoid
+ *     hitting the settings store
+ */
+export default function shouldHideEvent(ev: MatrixEvent, ctx?: IState): boolean {
+    // Accessing the settings store directly can be expensive if done frequently,
+    // so we should prefer using cached values if a RoomContext is available
+    const isEnabled = ctx ?
+        name => ctx[name] :
+        name => SettingsStore.getValue(name, ev.getRoomId());
 
     // Hide redacted events
     if (ev.isRedacted() && !isEnabled('showRedactions')) return true;

From 3bf8e54d7f37477868c1fa229449749bd02f2894 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sat, 5 Jun 2021 01:25:43 -0400
Subject: [PATCH 055/159] Use cached RoomContext settings values throughout
 rooms

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/structures/MessagePanel.js  | 5 ++++-
 src/components/structures/RoomView.tsx     | 2 +-
 src/components/structures/TimelinePanel.js | 5 ++++-
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 6709fef814..bca62e7b2f 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -26,6 +26,7 @@ import * as sdk from '../../index';
 
 import {MatrixClientPeg} from '../../MatrixClientPeg';
 import SettingsStore from '../../settings/SettingsStore';
+import RoomContext from "../../contexts/RoomContext";
 import {Layout, LayoutPropType} from "../../settings/Layout";
 import {_t} from "../../languageHandler";
 import {haveTileForEvent} from "../views/rooms/EventTile";
@@ -152,6 +153,8 @@ export default class MessagePanel extends React.Component {
         enableFlair: PropTypes.bool,
     };
 
+    static contextType = RoomContext;
+
     constructor(props) {
         super(props);
 
@@ -381,7 +384,7 @@ export default class MessagePanel extends React.Component {
         // Always show highlighted event
         if (this.props.highlightedEventId === mxEv.getId()) return true;
 
-        return !shouldHideEvent(mxEv);
+        return !shouldHideEvent(mxEv, this.context);
     }
 
     _readMarkerForEvent(eventId, isLastEvent) {
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index fa67013ccb..b80d909a94 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -859,7 +859,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             // update unread count when scrolled up
             if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
                 // no change
-            } else if (!shouldHideEvent(ev)) {
+            } else if (!shouldHideEvent(ev, this.state)) {
                 this.setState((state, props) => {
                     return {numUnreadMessages: state.numUnreadMessages + 1};
                 });
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 6300c7532e..c5ae2e87ba 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -26,6 +26,7 @@ import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
 import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
 import { _t } from '../../languageHandler';
 import {MatrixClientPeg} from "../../MatrixClientPeg";
+import RoomContext from "../../contexts/RoomContext";
 import UserActivity from "../../UserActivity";
 import Modal from "../../Modal";
 import dis from "../../dispatcher/dispatcher";
@@ -122,6 +123,8 @@ class TimelinePanel extends React.Component {
         layout: LayoutPropType,
     }
 
+    static contextType = RoomContext;
+
     // a map from room id to read marker event timestamp
     static roomReadMarkerTsMap = {};
 
@@ -1285,7 +1288,7 @@ class TimelinePanel extends React.Component {
 
             const shouldIgnore = !!ev.status || // local echo
                 (ignoreOwn && ev.sender && ev.sender.userId == myUserId);   // own message
-            const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev);
+            const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
 
             if (isWithoutTile || !node) {
                 // don't start counting if the event should be ignored,

From c24b239478aef6e4e19e3651a9f8376753f17f12 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 5 Jun 2021 13:36:25 +0000
Subject: [PATCH 056/159] Bump ws from 6.2.1 to 6.2.2 in /test/end-to-end-tests

Bumps [ws](https://github.com/websockets/ws) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/commits)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

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

diff --git a/test/end-to-end-tests/yarn.lock b/test/end-to-end-tests/yarn.lock
index 97b348fe50..bc942c4f51 100644
--- a/test/end-to-end-tests/yarn.lock
+++ b/test/end-to-end-tests/yarn.lock
@@ -760,9 +760,9 @@ wrappy@1:
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
 ws@^6.1.0:
-  version "6.2.1"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
-  integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
+  integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
   dependencies:
     async-limiter "~1.0.0"
 

From f63a92b5cf4c420ddcc652d587dee5fc446bfdd2 Mon Sep 17 00:00:00 2001
From: random <dictionary@tutamail.com>
Date: Fri, 4 Jun 2021 09:24:41 +0000
Subject: [PATCH 057/159] Translated using Weblate (Italian)

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

diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 585ee8ba3a..c83800e82a 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -3375,5 +3375,11 @@
     "Kick, ban, or invite people to your active room, and make you leave": "Buttare fuori, bandire o invitare persone nella tua stanza attiva e farti uscire",
     "See when people join, leave, or are invited to this room": "Vedere quando le persone entrano, escono o sono invitate in questa stanza",
     "Kick, ban, or invite people to this room, and make you leave": "Buttare fuori, bandire o invitare persone in questa stanza e farti uscire",
-    "See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva"
+    "See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva",
+    "Currently joining %(count)s rooms|one": "Stai entrando in %(count)s stanza",
+    "Currently joining %(count)s rooms|other": "Stai entrando in %(count)s stanze",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prova parole diverse o controlla errori di battitura. Alcuni risultati potrebbero non essere visibili dato che sono privati e ti servirebbe un invito per unirti.",
+    "No results for \"%(query)s\"": "Nessun risultato per \"%(query)s\"",
+    "The user you called is busy.": "L'utente che hai chiamato è occupato.",
+    "User Busy": "Utente occupato"
 }

From a85e251f0a6ceb0444e3f2de41471803ba07602c Mon Sep 17 00:00:00 2001
From: iaiz <git@iapellaniz.com>
Date: Tue, 1 Jun 2021 21:21:42 +0000
Subject: [PATCH 058/159] Translated using Weblate (Spanish)

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

diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json
index 60f5d06bec..5e8e57bacd 100644
--- a/src/i18n/strings/es.json
+++ b/src/i18n/strings/es.json
@@ -3315,5 +3315,11 @@
     "See when people join, leave, or are invited to your active room": "Ver cuando alguien se una, salga o se le invite a tu sala activa",
     "Kick, ban, or invite people to this room, and make you leave": "Expulsar, vetar o invitar personas a esta sala, y hacerte salir de ella",
     "Kick, ban, or invite people to your active room, and make you leave": "Expulsar, vetar o invitar a gente a tu sala activa, o hacerte salir",
-    "See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala"
+    "See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prueba con sinónimos o revisa si te has equivocado al escribir. Puede que algunos resultados no sean visibles si son privados y necesites que te inviten para verlos.",
+    "Currently joining %(count)s rooms|one": "Entrando en %(count)s sala",
+    "Currently joining %(count)s rooms|other": "Entrando en %(count)s salas",
+    "No results for \"%(query)s\"": "Ningún resultado para «%(query)s»",
+    "The user you called is busy.": "La persona a la que has llamado está ocupada.",
+    "User Busy": "Persona ocupada"
 }

From d7de5dfe833cd1ea46faa6c0fcd5e273435d5404 Mon Sep 17 00:00:00 2001
From: libexus <Asterixeins324@gmail.com>
Date: Fri, 4 Jun 2021 13:47:33 +0000
Subject: [PATCH 059/159] Translated using Weblate (German)

Currently translated at 99.4% (2962 of 2979 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 | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index dcc5343af4..9d1c1fa071 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -3343,5 +3343,14 @@
     "Your feedback will help make spaces better. The more detail you can go into, the better.": "Dein Feedback hilfst uns, die Spaces zu verbessern. Je genauer, desto besser.",
     "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Durchs Verlassen lädt %(brand)s mit deaktivierten Spaces neu. Danach kannst du Communities und Custom Tags wieder verwenden.",
     "sends space invaders": "sendet Space Invaders",
-    "Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen"
+    "Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen",
+    "Space Autocomplete": "Spaces automatisch vervollständigen",
+    "Currently joining %(count)s rooms|one": "Betrete %(count)s Raum",
+    "Currently joining %(count)s rooms|other": "Betrete %(count)s Räume",
+    "Go to my space": "Zu meinem Space",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Überprüfe auf Tippfehler oder verwende andere Suchbegriffe. Beachte, dass Ergebnisse aus privaten Räumen, in die du nicht eingeladen wurdest, nicht angezeigt werden.",
+    "See when people join, leave, or are invited to this room": "Anzeigen, wenn Leute eingeladen werden oder den Raum betreten und verlassen",
+    "The user you called is busy.": "Der angerufene Benutzer ist momentan beschäftigt.",
+    "User Busy": "Benutzer beschäftigt",
+    "No results for \"%(query)s\"": "Keine Ergebnisse für \"%(query)s\""
 }

From 7abd8957a5e2ff0be597d6435aea5f3f16a80e4a Mon Sep 17 00:00:00 2001
From: Jeff Huang <s8321414@gmail.com>
Date: Wed, 2 Jun 2021 02:39:11 +0000
Subject: [PATCH 060/159] Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (2979 of 2979 strings)

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

diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 5c27fb3878..053839f937 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -3378,5 +3378,11 @@
     "See when people join, leave, or are invited to your active room": "檢視人們何時加入、離開或被邀請至您活躍的聊天室",
     "Kick, ban, or invite people to your active room, and make you leave": "踢除、封鎖或邀請人們到您作用中的聊天室,然後讓您離開",
     "See when people join, leave, or are invited to this room": "檢視人們何時加入、離開或被邀請至此聊天室",
-    "Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開"
+    "Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開",
+    "Currently joining %(count)s rooms|one": "目前正在加入 %(count)s 個聊天室",
+    "Currently joining %(count)s rooms|other": "目前正在加入 %(count)s 個聊天室",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "嘗試不同的詞或是檢查拼字。某些結果可能不可見,因為其為私人的,您必須要有邀請才能加入。",
+    "No results for \"%(query)s\"": "「%(query)s」沒有結果",
+    "The user you called is busy.": "您想要通話的使用者目前忙碌中。",
+    "User Busy": "使用者忙碌"
 }

From 2533a9f52eb334bdb316ad9ebba4824693f940cb Mon Sep 17 00:00:00 2001
From: LinAGKar <linus.kardell@gmail.com>
Date: Tue, 1 Jun 2021 18:33:41 +0000
Subject: [PATCH 061/159] Translated using Weblate (Swedish)

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

diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index a50c039e9e..f026e0fe02 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -3305,5 +3305,11 @@
     "See when people join, leave, or are invited to your active room": "Se när folk går med, lämnar eller bjuds in till ditt aktiva rum",
     "Kick, ban, or invite people to your active room, and make you leave": "Kicka, banna eller bjuda in folk till ditt aktiva rum, och tvinga dig att lämna",
     "See when people join, leave, or are invited to this room": "Se när folk går med, lämnar eller bjuds in till det här rummet",
-    "Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna"
+    "Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna",
+    "Currently joining %(count)s rooms|one": "Går just nu med i %(count)s rum",
+    "Currently joining %(count)s rooms|other": "Går just nu med i %(count)s rum",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Testa andra ord eller kolla efter felskrivningar. Vissa resultat kanske inte visas för att de är privata och du behöver en inbjudan för att gå med i dem.",
+    "No results for \"%(query)s\"": "Inga resultat för \"%(query)s\"",
+    "The user you called is busy.": "Användaren du ringde är upptagen.",
+    "User Busy": "Användare upptagen"
 }

From 6243e693d6c3a14e28bc4c3ecada911a17859a80 Mon Sep 17 00:00:00 2001
From: jelv <post@jelv.nl>
Date: Wed, 2 Jun 2021 09:39:49 +0000
Subject: [PATCH 062/159] Translated using Weblate (Dutch)

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

diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 16f74e7b2d..7299e9d161 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -3261,5 +3261,11 @@
     "See when people join, leave, or are invited to your active room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd in uw actieve gesprek",
     "Kick, ban, or invite people to your active room, and make you leave": "Verwijder, verban of nodig personen uit voor uw actieve gesprek en uzelf laten vertrekken",
     "See when people join, leave, or are invited to this room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd voor dit gesprek",
-    "Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken"
+    "Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken",
+    "Currently joining %(count)s rooms|one": "Momenteel aan het toetreden tot %(count)s gesprek",
+    "Currently joining %(count)s rooms|other": "Momenteel aan het toetreden tot %(count)s gesprekken",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Probeer andere woorden of controleer op typefouten. Sommige resultaten zijn mogelijk niet zichtbaar omdat ze privé zijn of u een uitnodiging nodig heeft om deel te nemen.",
+    "No results for \"%(query)s\"": "Geen resultaten voor \"%(query)s\"",
+    "The user you called is busy.": "De gebruiker die u belde is bezet.",
+    "User Busy": "Gebruiker Bezet"
 }

From fe9644baca95f5e658684eeb5e3dd09d83c4491f Mon Sep 17 00:00:00 2001
From: Ihor Hordiichuk <igor_ck@outlook.com>
Date: Wed, 2 Jun 2021 00:41:15 +0000
Subject: [PATCH 063/159] Translated using Weblate (Ukrainian)

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

diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index db5ce9b360..64ba84b678 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -267,8 +267,8 @@
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s",
     "Who would you like to add to this community?": "Кого ви хочете додати до цієї спільноти?",
     "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Там, де ця сторінка містить ототожненну інформацію, як-от назва кімнати, користувача чи групи, ці дані будуть вилучені перед надсиланням на сервер.",
-    "Call in Progress": "Іде виклик",
-    "A call is currently being placed!": "Зараз іде виклик!",
+    "Call in Progress": "Триває виклик",
+    "A call is currently being placed!": "Зараз триває виклик!",
     "A call is already in progress!": "Вже здійснюється дзвінок!",
     "Permission Required": "Потрібен дозвіл",
     "You do not have permission to start a conference call in this room": "У вас немає дозволу, щоб розпочати дзвінок-конференцію в цій кімнаті",
@@ -1602,5 +1602,7 @@
     "Share Link to User": "Поділитися посиланням на користувача",
     "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Повідомлення тут захищено наскрізним шифруванням. Підтвердьте %(displayName)s у їхньому профілі — натиснувши на їх аватар.",
     "Open": "Відкрити",
-    "<a>In reply to</a> <pill>": "<a>У відповідь на</a> <pill>"
+    "<a>In reply to</a> <pill>": "<a>У відповідь на</a> <pill>",
+    "The user you called is busy.": "Користувач, якого ви викликаєте, зайнятий.",
+    "User Busy": "Користувач зайнятий"
 }

From f5b02ecbf55323af1dcdd057da79ba0801495193 Mon Sep 17 00:00:00 2001
From: XoseM <correoxm@disroot.org>
Date: Thu, 3 Jun 2021 06:53:34 +0000
Subject: [PATCH 064/159] Translated using Weblate (Galician)

Currently translated at 100.0% (2979 of 2979 strings)

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

diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 12a2dcd8c3..abb4776a55 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -3375,5 +3375,11 @@
     "See when people join, leave, or are invited to your active room": "Mira cando alguén se une, sae ou é convidada á túa sala activa",
     "Kick, ban, or invite people to your active room, and make you leave": "Expulsa, veta ou convida a persoas á túa sala activa, e fai que saias",
     "See when people join, leave, or are invited to this room": "Mira cando se une alguén, sae ou é convidada a esta sala",
-    "Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias"
+    "Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias",
+    "Currently joining %(count)s rooms|one": "Neste intre estás en %(count)s sala",
+    "Currently joining %(count)s rooms|other": "Neste intre estás en %(count)s salas",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Intentao con outras palabras e fíxate nos erros de escritura. Algúns resultados poderían non ser visibles porque son privados e precisas un convite.",
+    "No results for \"%(query)s\"": "Sen resultados para \"%(query)s\"",
+    "The user you called is busy.": "A persoa á que chamas está ocupada.",
+    "User Busy": "Usuaria ocupada"
 }

From af2d902830c7ab2dd656152bef924602142d4406 Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Wed, 2 Jun 2021 08:30:52 +0000
Subject: [PATCH 065/159] Translated using Weblate (Albanian)

Currently translated at 99.6% (2970 of 2979 strings)

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

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index bd294093f9..bbeffefda3 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -3361,5 +3361,12 @@
     "sends space invaders": "dërgon pushtues hapësire",
     "Sends the given message with a space themed effect": "E dërgon mesazhin e dhënë me një efekt teme hapësinore",
     "See when people join, leave, or are invited to your active room": "Shihni kur persona vijnë, ikin ose janë ftuar në dhomën tuaj aktive",
-    "See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë"
+    "See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë",
+    "Space Autocomplete": "Vetëplotësim Hapësire",
+    "Currently joining %(count)s rooms|one": "Aktualisht duke hyrë në %(count)s dhomë",
+    "Currently joining %(count)s rooms|other": "Aktualisht duke hyrë në %(count)s dhoma",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provoni fjalë të ndryshme, ose kontrolloni për gabime shkrimi. Disa përfundime mund të mos jenë të dukshme, ngaqë janë private dhe ju duhet një ftesë për të marrë pjesë në to.",
+    "No results for \"%(query)s\"": "S’ka përfundime për \"%(query)s\"",
+    "The user you called is busy.": "Përdoruesi që thirrët është i zënë.",
+    "User Busy": "Përdoruesi Është i Zënë"
 }

From 3bdc84bb5d164e37f69a9f45e7ee34f7dcb51089 Mon Sep 17 00:00:00 2001
From: waclaw66 <waclaw66@seznam.cz>
Date: Wed, 2 Jun 2021 06:54:33 +0000
Subject: [PATCH 066/159] Translated using Weblate (Czech)

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

diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index 75472b4d38..a84a10f21c 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -3292,5 +3292,11 @@
     "See when people join, leave, or are invited to your active room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do vaší aktivní místnosti",
     "Kick, ban, or invite people to this room, and make you leave": "Vykopnout, vykázat, pozvat lidi do této místnosti nebo odejít",
     "Kick, ban, or invite people to your active room, and make you leave": "Vykopnout, vykázat, pozvat lidi do vaší aktivní místnosti nebo odejít",
-    "See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti"
+    "See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti",
+    "Currently joining %(count)s rooms|one": "Momentálně se připojuje %(count)s místnost",
+    "Currently joining %(count)s rooms|other": "Momentálně se připojuje %(count)s místností",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Vyzkoušejte jiná slova nebo zkontrolujte překlepy. Některé výsledky nemusí být viditelné, protože jsou soukromé a potřebujete k nim pozvánku.",
+    "No results for \"%(query)s\"": "Žádné výsledky pro \"%(query)s\"",
+    "The user you called is busy.": "Volaný uživatel je zaneprázdněn.",
+    "User Busy": "Uživatel zaneprázdněn"
 }

From 261c91211fa0ad914e59ed27cdbd411ca1de65d5 Mon Sep 17 00:00:00 2001
From: Szimszon <github@oregpreshaz.eu>
Date: Wed, 2 Jun 2021 06:12:00 +0000
Subject: [PATCH 067/159] Translated using Weblate (Hungarian)

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

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index a6e9992866..e1ccfb288d 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -3370,5 +3370,11 @@
     "See when people join, leave, or are invited to your active room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése az aktív szobájában",
     "Kick, ban, or invite people to your active room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket az aktív szobába és, hogy ön elhagyja a szobát",
     "See when people join, leave, or are invited to this room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése ebben a szobában",
-    "Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát"
+    "Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát",
+    "Currently joining %(count)s rooms|one": "%(count)s szobába lép be",
+    "Currently joining %(count)s rooms|other": "%(count)s szobába lép be",
+    "No results for \"%(query)s\"": "Nincs találat ehhez: %(query)s",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Próbáljon ki más szavakat vagy keressen elgépelést. Néhány találat azért nem látszik, mert privát és meghívóra van szüksége, hogy csatlakozhasson.",
+    "The user you called is busy.": "A hívott felhasználó foglalt.",
+    "User Busy": "Felhasználó foglalt"
 }

From 0de8e9b201107ca76d2271d2091f586ba8a00899 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= <riot@joeruut.com>
Date: Tue, 1 Jun 2021 17:36:31 +0000
Subject: [PATCH 068/159] Translated using Weblate (Estonian)

Currently translated at 99.8% (2974 of 2979 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 | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 5e8d744cca..a8e267fb01 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -3345,5 +3345,14 @@
     "Send and receive voice messages": "Saada ja võta vastu häälsõnumeid",
     "Your feedback will help make spaces better. The more detail you can go into, the better.": "Sinu tagasiside aitab teha kogukonnakeskuseid paremaks. Mida detailsemalt sa oma arvamust kirjeldad, seda parem.",
     "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.",
-    "Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud"
+    "Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud",
+    "sends space invaders": "korraldab ühe pisikese tulnukate vallutusretke",
+    "Sends the given message with a space themed effect": "Saadab antud sõnumi kosmoseteemalise efektiga",
+    "Go to my space": "Palun vaata minu kogukonnakeskust",
+    "User Busy": "Kasutaja on hõivatud",
+    "The user you called is busy.": "Kasutaja, kellele sa helistasid, on hõivatud.",
+    "No results for \"%(query)s\"": "Päringule „%(query)s“ pole vastuseid",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Proovi muid otsingusõnu või kontrolli, et neis polnud trükivigu. Kuna mõned otsingutulemused on privaatsed ja sa vajad kutset nende nägemiseks, siis kõiki tulemusi siin ei pruugi näha olla.",
+    "Currently joining %(count)s rooms|other": "Parasjagu liitun %(count)s jututoaga",
+    "Currently joining %(count)s rooms|one": "Parasjagu liitun %(count)s jututoaga"
 }

From 0ecc97eb4b830f3cbaa09ee8c21617ca5eb5e067 Mon Sep 17 00:00:00 2001
From: Thibault Martin <mail@thibaultmart.in>
Date: Thu, 3 Jun 2021 07:09:49 +0000
Subject: [PATCH 069/159] Translated using Weblate (French)

Currently translated at 100.0% (2979 of 2979 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 7f3614bf90..e199e094e7 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -2842,7 +2842,7 @@
     "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 autocollants dans ce salon",
-    "Remain on your screen when viewing another room, when running": "Reste sur votre écran lors de l'appel quand vous regardez un autre salon",
+    "Remain on your screen when viewing another room, when running": "Reste sur votre écran lors de l’appel quand vous regardez un autre 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",

From 442d7f7a02ee6a0e0c3600e37aed08c289e6b32b Mon Sep 17 00:00:00 2001
From: Trendyne <eiko@chiru.no>
Date: Thu, 3 Jun 2021 09:58:35 +0000
Subject: [PATCH 070/159] Translated using Weblate (Icelandic)

Currently translated at 22.4% (668 of 2979 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 | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json
index 35f5342b30..e8718c941a 100644
--- a/src/i18n/strings/is.json
+++ b/src/i18n/strings/is.json
@@ -720,5 +720,13 @@
     "%(count)s messages deleted.|one": "%(count)s skilaboð eytt.",
     "%(count)s messages deleted.|other": "%(count)s skilaboðum eytt.",
     "Message deleted on %(date)s": "Skilaboð eytt á %(date)s",
-    "Message edits": "Skilaboðs breytingar"
+    "Message edits": "Skilaboðs breytingar",
+    "List options": "Lista valkosti",
+    "Create a Group Chat": "Búa Til Hópspjall",
+    "Explore Public Rooms": "Kanna Almenningsherbergi",
+    "Explore public rooms": "Kanna almenningsherbergi",
+    "Explore all public rooms": "Kanna öll almenningsherbergi",
+    "Liberate your communication": "Frelsaðu samskipti þín",
+    "Welcome to <name/>": "Velkomin til <name/>",
+    "Welcome to %(appName)s": "Velkomin til %(appName)s"
 }

From 0f64f4d692f36287ace222eb13e07244fa6e1c63 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sat, 5 Jun 2021 10:49:44 -0400
Subject: [PATCH 071/159] Fix MessagePanel tests

Signed-off-by: Robin Townsend <robin@robin.town>
---
 test/components/structures/MessagePanel-test.js | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 5b466b4bb0..4f7fca1759 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -51,8 +51,20 @@ class WrappedMessagePanel extends React.Component {
     };
 
     render() {
+        const roomContext = {
+            room,
+            roomId: room.roomId,
+            canReact: true,
+            canReply: true,
+            showReadReceipts: true,
+            showRedactions: false,
+            showJoinLeaves: false,
+            showAvatarChanges: false,
+            showDisplaynameChanges: true,
+        };
+
         return <MatrixClientContext.Provider value={client}>
-            <RoomContext.Provider value={{ canReact: true, canReply: true, room, roomId: room.roomId }}>
+            <RoomContext.Provider value={roomContext}>
                 <MessagePanel room={room} {...this.props} resizeNotifier={this.state.resizeNotifier} />
             </RoomContext.Provider>
         </MatrixClientContext.Provider>;

From bbd5fab7b5345f3147f8392907fb114129b6c74e Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sat, 5 Jun 2021 11:15:06 -0400
Subject: [PATCH 072/159] Fix type check

As TypeScript points out, you can only set an id on HTML elements, not
arbitrary components.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/dialogs/ForwardDialog.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index cba292ea25..355c92240e 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -214,7 +214,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
             />
         </div>
         <hr />
-        <div className="mx_ForwardList">
+        <div className="mx_ForwardList" id="mx_ForwardList">
             <SearchBox
                 className="mx_textinput_icon mx_textinput_search"
                 placeholder={_t("Search for rooms or people")}
@@ -222,7 +222,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
                 autoComplete={true}
                 autoFocus={true}
             />
-            <AutoHideScrollbar className="mx_ForwardList_content" id="mx_ForwardList">
+            <AutoHideScrollbar className="mx_ForwardList_content">
                 { rooms.length > 0 ? (
                     <div className="mx_ForwardList_results">
                         { rooms.map(room =>

From e891d18fa264491fe81fbbcd893fa5c9fdc1d51a Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sat, 5 Jun 2021 21:41:28 -0400
Subject: [PATCH 073/159] Add my email to my copyright notices

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss           | 2 +-
 src/components/views/dialogs/ForwardDialog.tsx      | 2 +-
 test/components/views/dialogs/ForwardDialog-test.js | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index fc59259ca2..7422d39626 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2021 Robin Townsend.
+Copyright 2021 Robin Townsend <robin@robin.town>
 
 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/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 355c92240e..e97958973b 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2021 Robin Townsend.
+Copyright 2021 Robin Townsend <robin@robin.town>
 
 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/test/components/views/dialogs/ForwardDialog-test.js b/test/components/views/dialogs/ForwardDialog-test.js
index 004954c713..c50fb073bf 100644
--- a/test/components/views/dialogs/ForwardDialog-test.js
+++ b/test/components/views/dialogs/ForwardDialog-test.js
@@ -1,5 +1,5 @@
 /*
-Copyright 2021 Robin Townsend.
+Copyright 2021 Robin Townsend <robin@robin.town>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From 903d4d252a0c2ac6043ec24251d4c446f33d7990 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sun, 6 Jun 2021 23:06:56 -0400
Subject: [PATCH 074/159] Add optimized function to determine whether event has
 text to display

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/TextForEvent.js                       | 274 ++++++++++++----------
 src/components/structures/MessagePanel.js |   9 +-
 src/components/views/rooms/EventTile.tsx  |   4 +-
 3 files changed, 155 insertions(+), 132 deletions(-)

diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 86f9ff20f4..22ce0dd9cf 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -21,6 +21,10 @@ import SettingsStore from "./settings/SettingsStore";
 import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
 import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
 
+// These functions are frequently used just to check whether an event has
+// any text to display at all. For this reason they return deferred values
+// to avoid the expense of looking up translations when they're not needed.
+
 function textForMemberEvent(ev) {
     // XXX: SYJS-16 "sender is sometimes null for join messages"
     const senderName = ev.sender ? ev.sender.name : ev.getSender();
@@ -28,84 +32,84 @@ function textForMemberEvent(ev) {
     const prevContent = ev.getPrevContent();
     const content = ev.getContent();
 
-    const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
+    const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
     switch (content.membership) {
         case 'invite': {
             const threePidContent = content.third_party_invite;
             if (threePidContent) {
                 if (threePidContent.display_name) {
-                    return _t('%(targetName)s accepted the invitation for %(displayName)s.', {
+                    return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
                         targetName,
                         displayName: threePidContent.display_name,
                     });
                 } else {
-                    return _t('%(targetName)s accepted an invitation.', {targetName});
+                    return () => _t('%(targetName)s accepted an invitation.', {targetName});
                 }
             } else {
-                return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
+                return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
             }
         }
         case 'ban':
-            return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
+            return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
         case 'join':
             if (prevContent && prevContent.membership === 'join') {
                 if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
-                    return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
+                    return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
                         oldDisplayName: prevContent.displayname,
                         displayName: content.displayname,
                     });
                 } else if (!prevContent.displayname && content.displayname) {
-                    return _t('%(senderName)s set their display name to %(displayName)s.', {
+                    return () => _t('%(senderName)s set their display name to %(displayName)s.', {
                         senderName: ev.getSender(),
                         displayName: content.displayname,
                     });
                 } else if (prevContent.displayname && !content.displayname) {
-                    return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
+                    return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
                         senderName,
                         oldDisplayName: prevContent.displayname,
                     });
                 } else if (prevContent.avatar_url && !content.avatar_url) {
-                    return _t('%(senderName)s removed their profile picture.', {senderName});
+                    return () => _t('%(senderName)s removed their profile picture.', {senderName});
                 } else if (prevContent.avatar_url && content.avatar_url &&
                     prevContent.avatar_url !== content.avatar_url) {
-                    return _t('%(senderName)s changed their profile picture.', {senderName});
+                    return () => _t('%(senderName)s changed their profile picture.', {senderName});
                 } else if (!prevContent.avatar_url && content.avatar_url) {
-                    return _t('%(senderName)s set a profile picture.', {senderName});
+                    return () => _t('%(senderName)s set a profile picture.', {senderName});
                 } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
                     // This is a null rejoin, it will only be visible if the Labs option is enabled
-                    return _t("%(senderName)s made no change.", {senderName});
+                    return () => _t("%(senderName)s made no change.", {senderName});
                 } else {
-                    return "";
+                    return null;
                 }
             } else {
                 if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
-                return _t('%(targetName)s joined the room.', {targetName});
+                return () => _t('%(targetName)s joined the room.', {targetName});
             }
         case 'leave':
             if (ev.getSender() === ev.getStateKey()) {
                 if (prevContent.membership === "invite") {
-                    return _t('%(targetName)s rejected the invitation.', {targetName});
+                    return () => _t('%(targetName)s rejected the invitation.', {targetName});
                 } else {
-                    return _t('%(targetName)s left the room.', {targetName});
+                    return () => _t('%(targetName)s left the room.', {targetName});
                 }
             } else if (prevContent.membership === "ban") {
-                return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
+                return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
             } else if (prevContent.membership === "invite") {
-                return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
+                return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
                     senderName,
                     targetName,
-                }) + ' ' + reason;
+                }) + ' ' + getReason();
             } else if (prevContent.membership === "join") {
-                return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
+                return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
             } else {
-                return "";
+                return null;
             }
     }
 }
 
 function textForTopicEvent(ev) {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
-    return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
+    return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
         senderDisplayName,
         topic: ev.getContent().topic,
     });
@@ -115,16 +119,16 @@ function textForRoomNameEvent(ev) {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
 
     if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
-        return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
+        return () => _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
     }
     if (ev.getPrevContent().name) {
-        return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
+        return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
             senderDisplayName,
             oldRoomName: ev.getPrevContent().name,
             newRoomName: ev.getContent().name,
         });
     }
-    return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
+    return () => _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
         senderDisplayName,
         roomName: ev.getContent().name,
     });
@@ -132,19 +136,23 @@ function textForRoomNameEvent(ev) {
 
 function textForTombstoneEvent(ev) {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
-    return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
+    return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
 }
 
 function textForJoinRulesEvent(ev) {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     switch (ev.getContent().join_rule) {
         case "public":
-            return _t('%(senderDisplayName)s made the room public to whoever knows the link.', {senderDisplayName});
+            return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
+                senderDisplayName,
+            });
         case "invite":
-            return _t('%(senderDisplayName)s made the room invite only.', {senderDisplayName});
+            return () => _t('%(senderDisplayName)s made the room invite only.', {
+                senderDisplayName,
+            });
         default:
             // The spec supports "knock" and "private", however nothing implements these.
-            return _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
+            return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
                 senderDisplayName,
                 rule: ev.getContent().join_rule,
             });
@@ -155,12 +163,12 @@ function textForGuestAccessEvent(ev) {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     switch (ev.getContent().guest_access) {
         case "can_join":
-            return _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
+            return () => _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
         case "forbidden":
-            return _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
+            return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
         default:
             // There's no other options we can expect, however just for safety's sake we'll do this.
-            return _t('%(senderDisplayName)s changed guest access to %(rule)s', {
+            return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
                 senderDisplayName,
                 rule: ev.getContent().guest_access,
             });
@@ -175,17 +183,17 @@ function textForRelatedGroupsEvent(ev) {
     const removed = prevGroups.filter((g) => !groups.includes(g));
 
     if (added.length && !removed.length) {
-        return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
+        return () => _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
             senderDisplayName,
             groups: added.join(', '),
         });
     } else if (!added.length && removed.length) {
-        return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
+        return () => _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
             senderDisplayName,
             groups: removed.join(', '),
         });
     } else if (added.length && removed.length) {
-        return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
+        return () => _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
             '%(oldGroups)s in this room.', {
             senderDisplayName,
             newGroups: added.join(', '),
@@ -193,7 +201,7 @@ function textForRelatedGroupsEvent(ev) {
         });
     } else {
         // Don't bother rendering this change (because there were no changes)
-        return '';
+        return null;
     }
 }
 
@@ -207,11 +215,11 @@ function textForServerACLEvent(ev) {
         allow_ip_literals: !(prevContent.allow_ip_literals === false),
     };
 
-    let text = "";
+    let getText = null;
     if (prev.deny.length === 0 && prev.allow.length === 0) {
-        text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
+        getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
     } else {
-        text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
+        getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
     }
 
     if (!Array.isArray(current.allow)) {
@@ -220,21 +228,24 @@ function textForServerACLEvent(ev) {
 
     // If we know for sure everyone is banned, mark the room as obliterated
     if (current.allow.length === 0) {
-        return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used.");
+        return () => getText() + " " +
+            _t("🎉 All servers are banned from participating! This room can no longer be used.");
     }
 
-    return text;
+    return getText;
 }
 
 function textForMessageEvent(ev) {
-    const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
-    let message = senderDisplayName + ': ' + ev.getContent().body;
-    if (ev.getContent().msgtype === "m.emote") {
-        message = "* " + senderDisplayName + " " + message;
-    } else if (ev.getContent().msgtype === "m.image") {
-        message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
-    }
-    return message;
+    return () => {
+        const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
+        let message = senderDisplayName + ': ' + ev.getContent().body;
+        if (ev.getContent().msgtype === "m.emote") {
+            message = "* " + senderDisplayName + " " + message;
+        } else if (ev.getContent().msgtype === "m.image") {
+            message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
+        }
+        return message;
+    };
 }
 
 function textForCanonicalAliasEvent(ev) {
@@ -248,96 +259,100 @@ function textForCanonicalAliasEvent(ev) {
 
     if (!removedAltAliases.length && !addedAltAliases.length) {
         if (newAlias) {
-            return _t('%(senderName)s set the main address for this room to %(address)s.', {
+            return () => _t('%(senderName)s set the main address for this room to %(address)s.', {
                 senderName: senderName,
                 address: ev.getContent().alias,
             });
         } else if (oldAlias) {
-            return _t('%(senderName)s removed the main address for this room.', {
+            return () => _t('%(senderName)s removed the main address for this room.', {
                 senderName: senderName,
             });
         }
     } else if (newAlias === oldAlias) {
         if (addedAltAliases.length && !removedAltAliases.length) {
-            return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
+            return () => _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
                 senderName: senderName,
                 addresses: addedAltAliases.join(", "),
                 count: addedAltAliases.length,
             });
         } if (removedAltAliases.length && !addedAltAliases.length) {
-            return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
+            return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
                 senderName: senderName,
                 addresses: removedAltAliases.join(", "),
                 count: removedAltAliases.length,
             });
         } if (removedAltAliases.length && addedAltAliases.length) {
-            return _t('%(senderName)s changed the alternative addresses for this room.', {
+            return () => _t('%(senderName)s changed the alternative addresses for this room.', {
                 senderName: senderName,
             });
         }
     } else {
         // both alias and alt_aliases where modified
-        return _t('%(senderName)s changed the main and alternative addresses for this room.', {
+        return () => _t('%(senderName)s changed the main and alternative addresses for this room.', {
             senderName: senderName,
         });
     }
     // in case there is no difference between the two events,
     // say something as we can't simply hide the tile from here
-    return _t('%(senderName)s changed the addresses for this room.', {
+    return () => _t('%(senderName)s changed the addresses for this room.', {
         senderName: senderName,
     });
 }
 
 function textForCallAnswerEvent(event) {
-    const senderName = event.sender ? event.sender.name : _t('Someone');
-    const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
-    return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
+    return () => {
+        const senderName = event.sender ? event.sender.name : _t('Someone');
+        const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
+        return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
+    };
 }
 
 function textForCallHangupEvent(event) {
-    const senderName = event.sender ? event.sender.name : _t('Someone');
+    const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
     const eventContent = event.getContent();
-    let reason = "";
+    let getReason = () => "";
     if (!MatrixClientPeg.get().supportsVoip()) {
-        reason = _t('(not supported by this browser)');
+        getReason = () => _t('(not supported by this browser)');
     } else if (eventContent.reason) {
         if (eventContent.reason === "ice_failed") {
             // We couldn't establish a connection at all
-            reason = _t('(could not connect media)');
+            getReason = () => _t('(could not connect media)');
         } else if (eventContent.reason === "ice_timeout") {
             // We established a connection but it died
-            reason = _t('(connection failed)');
+            getReason = () => _t('(connection failed)');
         } else if (eventContent.reason === "user_media_failed") {
             // The other side couldn't open capture devices
-            reason = _t("(their device couldn't start the camera / microphone)");
+            getReason = () => _t("(their device couldn't start the camera / microphone)");
         } else if (eventContent.reason === "unknown_error") {
             // An error code the other side doesn't have a way to express
             // (as opposed to an error code they gave but we don't know about,
             // in which case we show the error code)
-            reason = _t("(an error occurred)");
+            getReason = () => _t("(an error occurred)");
         } else if (eventContent.reason === "invite_timeout") {
-            reason = _t('(no answer)');
+            getReason = () => _t('(no answer)');
         } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
             // workaround for https://github.com/vector-im/element-web/issues/5178
             // it seems Android randomly sets a reason of "user hangup" which is
             // interpreted as an error code :(
             // https://github.com/vector-im/riot-android/issues/2623
             // Also the correct hangup code as of VoIP v1 (with underscore)
-            reason = '';
+            getReason = () => '';
         } else {
-            reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
+            getReason = () => _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
         }
     }
-    return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason;
+    return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
 }
 
 function textForCallRejectEvent(event) {
-    const senderName = event.sender ? event.sender.name : _t('Someone');
-    return _t('%(senderName)s declined the call.', {senderName});
+    return () => {
+        const senderName = event.sender ? event.sender.name : _t('Someone');
+        return _t('%(senderName)s declined the call.', {senderName});
+    };
 }
 
 function textForCallInviteEvent(event) {
-    const senderName = event.sender ? event.sender.name : _t('Someone');
+    const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
     // FIXME: Find a better way to determine this from the event?
     let isVoice = true;
     if (event.getContent().offer && event.getContent().offer.sdp &&
@@ -350,13 +365,21 @@ function textForCallInviteEvent(event) {
     // can have a hard time translating those strings. In an effort to make translations easier
     // and more accurate, we break out the string-based variables to a couple booleans.
     if (isVoice && isSupported) {
-        return _t("%(senderName)s placed a voice call.", {senderName});
+        return () => _t("%(senderName)s placed a voice call.", {
+            senderName: getSenderName(),
+        });
     } else if (isVoice && !isSupported) {
-        return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName});
+        return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
+            senderName: getSenderName(),
+        });
     } else if (!isVoice && isSupported) {
-        return _t("%(senderName)s placed a video call.", {senderName});
+        return () => _t("%(senderName)s placed a video call.", {
+            senderName: getSenderName(),
+        });
     } else if (!isVoice && !isSupported) {
-        return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName});
+        return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
+            senderName: getSenderName(),
+        });
     }
 }
 
@@ -364,14 +387,13 @@ function textForThreePidInviteEvent(event) {
     const senderName = event.sender ? event.sender.name : event.getSender();
 
     if (!isValid3pidInvite(event)) {
-        const targetDisplayName = event.getPrevContent().display_name || _t("Someone");
-        return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
+        return () => _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
             senderName,
-            targetDisplayName,
+            targetDisplayName: event.getPrevContent().display_name || _t("Someone"),
         });
     }
 
-    return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
+    return () => _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
         senderName,
         targetDisplayName: event.getContent().display_name,
     });
@@ -381,17 +403,17 @@ function textForHistoryVisibilityEvent(event) {
     const senderName = event.sender ? event.sender.name : event.getSender();
     switch (event.getContent().history_visibility) {
         case 'invited':
-            return _t('%(senderName)s made future room history visible to all room members, '
+            return () => _t('%(senderName)s made future room history visible to all room members, '
                 + 'from the point they are invited.', {senderName});
         case 'joined':
-            return _t('%(senderName)s made future room history visible to all room members, '
+            return () => _t('%(senderName)s made future room history visible to all room members, '
                 + 'from the point they joined.', {senderName});
         case 'shared':
-            return _t('%(senderName)s made future room history visible to all room members.', {senderName});
+            return () => _t('%(senderName)s made future room history visible to all room members.', {senderName});
         case 'world_readable':
-            return _t('%(senderName)s made future room history visible to anyone.', {senderName});
+            return () => _t('%(senderName)s made future room history visible to anyone.', {senderName});
         default:
-            return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
+            return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
                 senderName,
                 visibility: event.getContent().history_visibility,
             });
@@ -403,7 +425,7 @@ function textForPowerEvent(event) {
     const senderName = event.sender ? event.sender.name : event.getSender();
     if (!event.getPrevContent() || !event.getPrevContent().users ||
         !event.getContent() || !event.getContent().users) {
-        return '';
+        return null;
     }
     const userDefault = event.getContent().users_default || 0;
     // Construct set of userIds
@@ -418,35 +440,35 @@ function textForPowerEvent(event) {
             if (users.indexOf(userId) === -1) users.push(userId);
         },
     );
-    const diff = [];
-    // XXX: This is also surely broken for i18n
+    const diffs = [];
     users.forEach((userId) => {
         // Previous power level
         const from = event.getPrevContent().users[userId];
         // Current power level
         const to = event.getContent().users[userId];
         if (to !== from) {
-            diff.push(
-                _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
-                    userId,
-                    fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
-                    toPowerLevel: Roles.textualPowerLevel(to, userDefault),
-                }),
-            );
+            diffs.push({ userId, from, to });
         }
     });
-    if (!diff.length) {
-        return '';
+    if (!diffs.length) {
+        return null;
     }
-    return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
+    // XXX: This is also surely broken for i18n
+    return () => _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
         senderName,
-        powerLevelDiffText: diff.join(", "),
+        powerLevelDiffText: diffs.map(diff =>
+            _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
+                userId: diff.userId,
+                fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault),
+                toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault),
+            }),
+        ).join(", "),
     });
 }
 
 function textForPinnedEvent(event) {
     const senderName = event.sender ? event.sender.name : event.getSender();
-    return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
+    return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
 }
 
 function textForWidgetEvent(event) {
@@ -464,16 +486,16 @@ function textForWidgetEvent(event) {
     // equivalent to that condition.
     if (url) {
         if (prevUrl) {
-            return _t('%(widgetName)s widget modified by %(senderName)s', {
+            return () => _t('%(widgetName)s widget modified by %(senderName)s', {
                 widgetName, senderName,
             });
         } else {
-            return _t('%(widgetName)s widget added by %(senderName)s', {
+            return () => _t('%(widgetName)s widget added by %(senderName)s', {
                 widgetName, senderName,
             });
         }
     } else {
-        return _t('%(widgetName)s widget removed by %(senderName)s', {
+        return () => _t('%(widgetName)s widget removed by %(senderName)s', {
             widgetName, senderName,
         });
     }
@@ -481,7 +503,7 @@ function textForWidgetEvent(event) {
 
 function textForWidgetLayoutEvent(event) {
     const senderName = event.sender?.name || event.getSender();
-    return _t("%(senderName)s has updated the widget layout", {senderName});
+    return () => _t("%(senderName)s has updated the widget layout", {senderName});
 }
 
 function textForMjolnirEvent(event) {
@@ -492,74 +514,74 @@ function textForMjolnirEvent(event) {
     // Rule removed
     if (!entity) {
         if (USER_RULE_TYPES.includes(event.getType())) {
-            return _t("%(senderName)s removed the rule banning users matching %(glob)s",
+            return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
                 {senderName, glob: prevEntity});
         } else if (ROOM_RULE_TYPES.includes(event.getType())) {
-            return _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
+            return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
                 {senderName, glob: prevEntity});
         } else if (SERVER_RULE_TYPES.includes(event.getType())) {
-            return _t("%(senderName)s removed the rule banning servers matching %(glob)s",
+            return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
                 {senderName, glob: prevEntity});
         }
 
         // Unknown type. We'll say something, but we shouldn't end up here.
-        return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
+        return () => _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
     }
 
     // Invalid rule
-    if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName});
+    if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, {senderName});
 
     // Rule updated
     if (entity === prevEntity) {
         if (USER_RULE_TYPES.includes(event.getType())) {
-            return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
+            return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
                 {senderName, glob: entity, reason});
         } else if (ROOM_RULE_TYPES.includes(event.getType())) {
-            return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
+            return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
                 {senderName, glob: entity, reason});
         } else if (SERVER_RULE_TYPES.includes(event.getType())) {
-            return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
+            return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
                 {senderName, glob: entity, reason});
         }
 
         // Unknown type. We'll say something but we shouldn't end up here.
-        return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
+        return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
             {senderName, glob: entity, reason});
     }
 
     // New rule
     if (!prevEntity) {
         if (USER_RULE_TYPES.includes(event.getType())) {
-            return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
+            return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
                 {senderName, glob: entity, reason});
         } else if (ROOM_RULE_TYPES.includes(event.getType())) {
-            return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
+            return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
                 {senderName, glob: entity, reason});
         } else if (SERVER_RULE_TYPES.includes(event.getType())) {
-            return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
+            return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
                 {senderName, glob: entity, reason});
         }
 
         // Unknown type. We'll say something but we shouldn't end up here.
-        return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
+        return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
             {senderName, glob: entity, reason});
     }
 
     // else the entity !== prevEntity - count as a removal & add
     if (USER_RULE_TYPES.includes(event.getType())) {
-        return _t(
+        return () => _t(
             "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
             "%(newGlob)s for %(reason)s",
             {senderName, oldGlob: prevEntity, newGlob: entity, reason},
         );
     } else if (ROOM_RULE_TYPES.includes(event.getType())) {
-        return _t(
+        return () => _t(
             "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
             "%(newGlob)s for %(reason)s",
             {senderName, oldGlob: prevEntity, newGlob: entity, reason},
         );
     } else if (SERVER_RULE_TYPES.includes(event.getType())) {
-        return _t(
+        return () => _t(
             "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
             "%(newGlob)s for %(reason)s",
             {senderName, oldGlob: prevEntity, newGlob: entity, reason},
@@ -567,7 +589,7 @@ function textForMjolnirEvent(event) {
     }
 
     // Unknown type. We'll say something but we shouldn't end up here.
-    return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
+    return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
         "for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
 }
 
@@ -604,8 +626,12 @@ for (const evType of ALL_RULE_TYPES) {
     stateHandlers[evType] = textForMjolnirEvent;
 }
 
+export function hasText(ev) {
+    const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
+    return Boolean(handler?.(ev));
+}
+
 export function textForEvent(ev) {
     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
-    if (handler) return handler(ev);
-    return '';
+    return handler?.(ev)?.() || '';
 }
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index bca62e7b2f..d040944833 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -30,7 +30,7 @@ import RoomContext from "../../contexts/RoomContext";
 import {Layout, LayoutPropType} from "../../settings/Layout";
 import {_t} from "../../languageHandler";
 import {haveTileForEvent} from "../views/rooms/EventTile";
-import {textForEvent} from "../../TextForEvent";
+import {hasText} from "../../TextForEvent";
 import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
 import DMRoomMap from "../../utils/DMRoomMap";
 import NewRoomIntro from "../views/rooms/NewRoomIntro";
@@ -1180,11 +1180,8 @@ class MemberGrouper {
 
     add(ev) {
         if (ev.getType() === 'm.room.member') {
-            // We'll just double check that it's worth our time to do so, through an
-            // ugly hack. If textForEvent returns something, we should group it for
-            // rendering but if it doesn't then we'll exclude it.
-            const renderText = textForEvent(ev);
-            if (!renderText || renderText.trim().length === 0) return; // quietly ignore
+            // We can ignore any events that don't actually have a message to display
+            if (!hasText(ev)) return;
         }
         this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
             ev.getId(),
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 8cec067c39..fda2906f07 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -25,7 +25,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
 
 import ReplyThread from "../elements/ReplyThread";
 import { _t } from '../../../languageHandler';
-import * as TextForEvent from "../../../TextForEvent";
+import { hasText } from "../../../TextForEvent";
 import * as sdk from "../../../index";
 import dis from '../../../dispatcher/dispatcher';
 import SettingsStore from "../../../settings/SettingsStore";
@@ -1200,7 +1200,7 @@ export function haveTileForEvent(e) {
     const handler = getHandlerTile(e);
     if (handler === undefined) return false;
     if (handler === 'messages.TextualEvent') {
-        return TextForEvent.textForEvent(e) !== '';
+        return hasText(e);
     } else if (handler === 'messages.RoomCreate') {
         return Boolean(e.getContent()['predecessor']);
     } else {

From 1e574307d0a74c498429c6e2a22f07c61369550b Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sun, 6 Jun 2021 23:08:47 -0400
Subject: [PATCH 075/159] Cache lowBandwidth setting to speed up BaseAvatar

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/structures/RoomView.tsx      |  5 +++++
 src/components/views/avatars/BaseAvatar.tsx | 15 +++++++++++----
 src/contexts/RoomContext.ts                 |  1 +
 3 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index b80d909a94..e4dc90f141 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -182,6 +182,7 @@ export interface IState {
     canReact: boolean;
     canReply: boolean;
     layout: Layout;
+    lowBandwidth: boolean;
     showReadReceipts: boolean;
     showRedactions: boolean;
     showJoinLeaves: boolean;
@@ -244,6 +245,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             canReact: false,
             canReply: false,
             layout: SettingsStore.getValue("layout"),
+            lowBandwidth: SettingsStore.getValue("lowBandwidth"),
             showReadReceipts: true,
             showRedactions: true,
             showJoinLeaves: true,
@@ -279,6 +281,9 @@ export default class RoomView extends React.Component<IProps, IState> {
             SettingsStore.watchSetting("layout", null, () =>
                 this.setState({ layout: SettingsStore.getValue("layout") }),
             ),
+            SettingsStore.watchSetting("lowBandwidth", null, () =>
+                this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
+            ),
         ];
     }
 
diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx
index 8ce05e0a55..6949c14636 100644
--- a/src/components/views/avatars/BaseAvatar.tsx
+++ b/src/components/views/avatars/BaseAvatar.tsx
@@ -22,6 +22,7 @@ import classNames from 'classnames';
 import * as AvatarLogic from '../../../Avatar';
 import SettingsStore from "../../../settings/SettingsStore";
 import AccessibleButton from '../elements/AccessibleButton';
+import RoomContext from "../../../contexts/RoomContext";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {useEventEmitter} from "../../../hooks/useEventEmitter";
 import {toPx} from "../../../utils/units";
@@ -44,12 +45,12 @@ interface IProps {
     className?: string;
 }
 
-const calculateUrls = (url, urls) => {
+const calculateUrls = (url, urls, lowBandwidth) => {
     // work out the full set of urls to try to load. This is formed like so:
     // imageUrls: [ props.url, ...props.urls ]
 
     let _urls = [];
-    if (!SettingsStore.getValue("lowBandwidth")) {
+    if (!lowBandwidth) {
         _urls = urls || [];
 
         if (url) {
@@ -63,7 +64,13 @@ const calculateUrls = (url, urls) => {
 };
 
 const useImageUrl = ({url, urls}): [string, () => void] => {
-    const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls));
+    // Since this is a hot code path and the settings store can be slow, we
+    // use the cached lowBandwidth value from the room context if it exists
+    const roomContext = useContext(RoomContext);
+    const lowBandwidth = roomContext ?
+        roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
+
+    const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
     const [urlsIndex, setIndex] = useState<number>(0);
 
     const onError = useCallback(() => {
@@ -71,7 +78,7 @@ const useImageUrl = ({url, urls}): [string, () => void] => {
     }, []);
 
     useEffect(() => {
-        setUrls(calculateUrls(url, urls));
+        setUrls(calculateUrls(url, urls, lowBandwidth));
         setIndex(0);
     }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
 
diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts
index 1efa1c03e7..3464f952a6 100644
--- a/src/contexts/RoomContext.ts
+++ b/src/contexts/RoomContext.ts
@@ -40,6 +40,7 @@ const RoomContext = createContext<IState>({
     canReact: false,
     canReply: false,
     layout: Layout.Group,
+    lowBandwidth: false,
     showReadReceipts: true,
     showRedactions: true,
     showJoinLeaves: true,

From 21ff1f521fa191308852429214d2697ee35adda8 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Sun, 6 Jun 2021 23:14:26 -0400
Subject: [PATCH 076/159] Fix i18n strings

Signed-off-by: Robin Townsend <robin@robin.town>
---
 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 9e85ea28c8..5108ea7fe2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -556,8 +556,8 @@
     "%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.",
     "%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.",
     "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).",
-    "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
     "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
+    "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
     "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
     "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
     "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",

From ed80db73d4209a518cb8828fa55aa0aec60dcd96 Mon Sep 17 00:00:00 2001
From: Tirifto <tirifto@posteo.cz>
Date: Sun, 6 Jun 2021 17:19:04 +0000
Subject: [PATCH 077/159] Translated using Weblate (Esperanto)

Currently translated at 99.9% (2978 of 2979 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 | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json
index fc8c00fd19..1aa5ba8a52 100644
--- a/src/i18n/strings/eo.json
+++ b/src/i18n/strings/eo.json
@@ -3320,5 +3320,11 @@
     "Reset event store": "Restarigi deponejon de okazoj",
     "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Se vi tamen tion faras, sciu ke neniu el viaj mesaĝoj foriĝos, sed via sperto pri serĉado povas malboniĝi momente, dum la indekso estas refarata",
     "You most likely do not want to reset your event index store": "Plej probable, vi ne volas restarigi vian deponejon de indeksoj de okazoj",
-    "Reset event store?": "Ĉu restarigi deponejon de okazoj?"
+    "Reset event store?": "Ĉu restarigi deponejon de okazoj?",
+    "Currently joining %(count)s rooms|one": "Nun aliĝante al %(count)s ĉambro",
+    "Currently joining %(count)s rooms|other": "Nun aliĝante al %(count)s ĉambroj",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provu aliajn vortojn aŭ kontorolu, ĉu vi ne tajperaris. Iuj rezultoj eble ne videblos, ĉar ili estas privataj kaj vi bezonus inviton por aliĝi.",
+    "No results for \"%(query)s\"": "Neniuj rezultoj por «%(query)s»",
+    "The user you called is busy.": "La uzanto, kiun vi vokis, estas okupata.",
+    "User Busy": "Uzanto estas okupata"
 }

From 4185185cdc9e97231ca8f25ca4c9a5041429225c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Mon, 7 Jun 2021 19:42:48 +0200
Subject: [PATCH 078/159] Reduce number of DOM elements
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/SenderProfile.tsx | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx
index a51f50525d..df75de34ba 100644
--- a/src/components/views/messages/SenderProfile.tsx
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -146,12 +146,10 @@ export default class SenderProfile extends React.Component<IProps, IState> {
         }
 
         return (
-            <div className="mx_SenderProfile" dir="auto" onClick={this.props.onClick}>
-                <div className="mx_SenderProfile_hover">
-                    { displayNameElement }
-                    { mxidElement }
-                    { flair }
-                </div>
+            <div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
+                { displayNameElement }
+                { mxidElement }
+                { flair }
             </div>
         );
     }

From b39c615abc6f2787decfcd5ed4c38ae6ea01d072 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Mon, 7 Jun 2021 19:44:58 +0200
Subject: [PATCH 079/159] Reorder
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/messages/SenderProfile.tsx          | 24 +++++++++----------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx
index df75de34ba..872dc132b9 100644
--- a/src/components/views/messages/SenderProfile.tsx
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -118,18 +118,6 @@ export default class SenderProfile extends React.Component<IProps, IState> {
             return null; // emote message must include the name so don't duplicate it
         }
 
-        let flair;
-        if (this.props.enableFlair) {
-            const displayedGroups = this._getDisplayedGroups(
-                this.state.userGroups, this.state.relatedGroups,
-            );
-
-            flair = <Flair key='flair'
-                userId={mxEvent.getSender()}
-                groups={displayedGroups}
-            />;
-        }
-
         const displayNameElement = (
             <span className={`mx_SenderProfile_displayName ${colorClass}`}>
                 { displayName }
@@ -145,6 +133,18 @@ export default class SenderProfile extends React.Component<IProps, IState> {
             );
         }
 
+        let flair;
+        if (this.props.enableFlair) {
+            const displayedGroups = this._getDisplayedGroups(
+                this.state.userGroups, this.state.relatedGroups,
+            );
+
+            flair = <Flair key='flair'
+                userId={mxEvent.getSender()}
+                groups={displayedGroups}
+            />;
+        }
+
         return (
             <div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
                 { displayNameElement }

From a5bf17ff659eeb8f3d915163217d9c8574d53f54 Mon Sep 17 00:00:00 2001
From: Aaron Raimist <aaron@raim.ist>
Date: Mon, 7 Jun 2021 16:54:36 -0500
Subject: [PATCH 080/159] Revert incorrect change to forShareableRoom

Signed-off-by: Aaron Raimist <aaron@raim.ist>
---
 src/utils/permalinks/Permalinks.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts
index 2f268aaa0c..c2f95defd6 100644
--- a/src/utils/permalinks/Permalinks.ts
+++ b/src/utils/permalinks/Permalinks.ts
@@ -293,12 +293,12 @@ export function makeRoomPermalink(roomId: string): string {
 
     // If the roomId isn't actually a room ID, don't try to list the servers.
     // Aliases are already routable, and don't need extra information.
-    if (roomId[0] !== '!') return getPermalinkConstructor().forShareableRoom(roomId, []);
+    if (roomId[0] !== '!') return getPermalinkConstructor().forRoom(roomId, []);
 
     const client = MatrixClientPeg.get();
     const room = client.getRoom(roomId);
     if (!room) {
-        return getPermalinkConstructor().forShareableRoom(roomId, []);
+        return getPermalinkConstructor().forRoom(roomId, []);
     }
     const permalinkCreator = new RoomPermalinkCreator(room);
     permalinkCreator.load();

From 773af6c7be6244775b3c2da79e38921381b401a9 Mon Sep 17 00:00:00 2001
From: Aaron Raimist <aaron@raim.ist>
Date: Mon, 7 Jun 2021 16:54:50 -0500
Subject: [PATCH 081/159] Fix test

Signed-off-by: Aaron Raimist <aaron@raim.ist>
---
 test/utils/permalinks/Permalinks-test.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

From 946416cf8756ac94d0229f038e561f9215ca695f Mon Sep 17 00:00:00 2001
From: Aaron Raimist <aaron@raim.ist>
Date: Mon, 7 Jun 2021 17:09:43 -0500
Subject: [PATCH 082/159] Make serverCandidates optional

Signed-off-by: Aaron Raimist <aaron@raim.ist>
---
 src/utils/permalinks/PermalinkConstructor.ts | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/utils/permalinks/PermalinkConstructor.ts b/src/utils/permalinks/PermalinkConstructor.ts
index 25855eb024..986d20d4d1 100644
--- a/src/utils/permalinks/PermalinkConstructor.ts
+++ b/src/utils/permalinks/PermalinkConstructor.ts
@@ -19,11 +19,11 @@ limitations under the License.
  * TODO: Convert this to a real TypeScript interface
  */
 export default class PermalinkConstructor {
-    forEvent(roomId: string, eventId: string, serverCandidates: string[]): string {
+    forEvent(roomId: string, eventId: string, serverCandidates: string[] = []): string {
         throw new Error("Not implemented");
     }
 
-    forRoom(roomIdOrAlias: string, serverCandidates: string[]): string {
+    forRoom(roomIdOrAlias: string, serverCandidates: string[] = []): string {
         throw new Error("Not implemented");
     }
 
@@ -73,12 +73,12 @@ export class PermalinkParts {
         return new PermalinkParts(null, null, null, groupId, null);
     }
 
-    static forRoom(roomIdOrAlias: string, viaServers: string[]): PermalinkParts {
-        return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers || []);
+    static forRoom(roomIdOrAlias: string, viaServers: string[] = []): PermalinkParts {
+        return new PermalinkParts(roomIdOrAlias, null, null, null, viaServers);
     }
 
-    static forEvent(roomId: string, eventId: string, viaServers: string[]): PermalinkParts {
-        return new PermalinkParts(roomId, eventId, null, null, viaServers || []);
+    static forEvent(roomId: string, eventId: string, viaServers: string[] = []): PermalinkParts {
+        return new PermalinkParts(roomId, eventId, null, null, viaServers);
     }
 
     get primaryEntityId(): string {

From a8dab04d5eed882f90244f0a5b1ec54cbfaaea58 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 7 Jun 2021 19:10:45 -0400
Subject: [PATCH 083/159] Remove mystery dot from forward dialog preview

It was a bullet point, since EventTiles now get created as li by default
:P

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

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index e97958973b..b89485943a 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -211,6 +211,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
                 layout={previewLayout}
                 enableFlair={flairEnabled}
                 permalinkCreator={permalinkCreator}
+                as="div"
             />
         </div>
         <hr />

From b00ad63a35fa2aa18c0ca04b803b34e252fa270c Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 7 Jun 2021 19:16:00 -0400
Subject: [PATCH 084/159] Fix whitespace nitpick

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/dialogs/ForwardDialog.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index b89485943a..1c90dca432 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -201,7 +201,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
         onFinished={onFinished}
         fixedWidth={false}
     >
-        <h3>{_t("Message preview")}</h3>
+        <h3>{ _t("Message preview") }</h3>
         <div className={classnames("mx_ForwardDialog_preview", {
             "mx_IRCLayout": previewLayout == Layout.IRC,
             "mx_GroupLayout": previewLayout == Layout.Group,

From 372f24ff99476685cbc1c953f8b66951ce13b149 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Mon, 7 Jun 2021 19:19:14 -0400
Subject: [PATCH 085/159] Hide download links from forward dialog preview

since trying to interact with them is pointless.

Signed-off-by: Robin Townsend <robin@robin.town>
---
 res/css/views/dialogs/_ForwardDialog.scss | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index 7422d39626..1976a43ab9 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -49,6 +49,11 @@ limitations under the License.
         .mx_EventTile_e2eIcon_unencrypted {
             display: none;
         }
+
+        // We also hide download links to not encourage users to try interacting
+        .mx_MFileBody_download {
+            display: none;
+        }
     }
 
     > hr {

From 26a030abcd76daf27c9f360000e20d54fc65cb19 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 8 Jun 2021 12:40:38 +0100
Subject: [PATCH 086/159] Better handling for widgets that fail to load

---
 src/components/views/elements/AppTile.js | 40 +++++++++++++++++-------
 1 file changed, 28 insertions(+), 12 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index b898ad2ebc..91ffbf2c27 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
 
         // The key used for PersistedElement
         this._persistKey = getPersistKey(this.props.app.id);
-        this._sgWidget = new StopGapWidget(this.props);
-        this._sgWidget.on("preparing", this._onWidgetPrepared);
-        this._sgWidget.on("ready", this._onWidgetReady);
+        try {
+            this._sgWidget = new StopGapWidget(this.props);
+            this._sgWidget.on("preparing", this._onWidgetPrepared);
+            this._sgWidget.on("ready", this._onWidgetReady);
+        } catch (e) {
+            console.log("Failed to construct widget", e);
+            this._sgWidget = null;
+        }
         this.iframe = null; // ref to the iframe (callback style)
 
         this.state = this._getNewState(props);
@@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
             // Force the widget to be non-persistent (able to be deleted/forgotten)
             ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
             PersistedElement.destroyElement(this._persistKey);
-            this._sgWidget.stop();
+            if (this._sgWidget) this._sgWidget.stop();
         }
 
         this.setState({ hasPermissionToLoad });
@@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
 
     componentDidMount() {
         // Only fetch IM token on mount if we're showing and have permission to load
-        if (this.state.hasPermissionToLoad) {
+        if (this._sgWidget && this.state.hasPermissionToLoad) {
             this._startWidget();
         }
 
@@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
         if (this._sgWidget) {
             this._sgWidget.stop();
         }
-        this._sgWidget = new StopGapWidget(newProps);
-        this._sgWidget.on("preparing", this._onWidgetPrepared);
-        this._sgWidget.on("ready", this._onWidgetReady);
-        this._startWidget();
+        try {
+            this._sgWidget = new StopGapWidget(newProps);
+            this._sgWidget.on("preparing", this._onWidgetPrepared);
+            this._sgWidget.on("ready", this._onWidgetReady);
+            this._startWidget();
+        } catch (e) {
+            console.log("Failed to construct widget", e);
+            this._sgWidget = null;
+        }
     }
 
     _startWidget() {
@@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
     _iframeRefChange = (ref) => {
         this.iframe = ref;
         if (ref) {
-            this._sgWidget.start(ref);
+            if (this._sgWidget) this._sgWidget.start(ref);
         } else {
             this._resetWidget(this.props);
         }
@@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
         // Delete the widget from the persisted store for good measure.
         PersistedElement.destroyElement(this._persistKey);
 
-        this._sgWidget.stop({forceDestroy: true});
+        if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
     }
 
     _onWidgetPrepared = () => {
@@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
                 <Spinner message={_t("Loading...")} />
             </div>
         );
-        if (!this.state.hasPermissionToLoad) {
+        if (this._sgWidget === null) {
+            appTileBody = (
+                <div className={appTileBodyClass} style={appTileBodyStyles}>
+                    <AppWarning errorMsg="Error loading Widget" />
+                </div>
+            );
+        } else if (!this.state.hasPermissionToLoad) {
             // only possible for room widgets, can assert this.props.room here
             const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
             appTileBody = (

From 5c62728d13ac23721c7747c10929e38fa4f2458b Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 8 Jun 2021 12:52:32 +0100
Subject: [PATCH 087/159] Migrate end to end tests pipeline to GitHub Actions

---
 .github/workflows/end-to-end-tests.yml | 32 ++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)
 create mode 100644 .github/workflows/end-to-end-tests.yml

diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml
new file mode 100644
index 0000000000..271ea06529
--- /dev/null
+++ b/.github/workflows/end-to-end-tests.yml
@@ -0,0 +1,32 @@
+name: "end-to-end_tests"
+on: [push]
+jobs:
+    "end-to-end_tests":
+        runs-on: ubuntu-latest
+        defaults:
+            run:
+                shell: bash
+                working-directory: ${{ github.workspace }}
+        container:
+            image: vectorim/element-web-ci-e2etests-env:latest
+            env:
+                CI_PACKAGE: true
+                GITHUB_HEAD_REF: ${{ github.head_ref }}
+                GITHUB_BASE_REF: ${{ github.base_ref }}
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v2
+            - name: End-to-End tests
+              run: ./scripts/ci/end-to-end-tests.sh
+            - name: Archive logs
+              uses: actions/upload-artifact@v2
+              with:
+                path: |
+                    test/end-to-end-tests/logs/**/*
+                    test/end-to-end-tests/synapse/installations/consent/homeserver.log
+                retention-days: 14
+            - name: Archive performance benchmark
+              uses: actions/upload-artifact@v2
+              with:
+                name: performance-entries.json
+                path: test/end-to-end-tests/performance-entries.json

From 90a58380fd3732f250f63de65a4d542b0a0a6837 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 8 Jun 2021 16:15:20 +0100
Subject: [PATCH 088/159] Make it translateable

And also the other one that I copied from
---
 src/components/views/elements/AppTile.js | 4 ++--
 src/i18n/strings/en_EN.json              | 2 ++
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 91ffbf2c27..4028909167 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -353,7 +353,7 @@ export default class AppTile extends React.Component {
         if (this._sgWidget === null) {
             appTileBody = (
                 <div className={appTileBodyClass} style={appTileBodyStyles}>
-                    <AppWarning errorMsg="Error loading Widget" />
+                    <AppWarning errorMsg={_t("Error loading Widget")} />
                 </div>
             );
         } else if (!this.state.hasPermissionToLoad) {
@@ -380,7 +380,7 @@ export default class AppTile extends React.Component {
             if (this.isMixedContent()) {
                 appTileBody = (
                     <div className={appTileBodyClass} style={appTileBodyStyles}>
-                        <AppWarning errorMsg="Error - Mixed content" />
+                        <AppWarning errorMsg={_t("Error - Mixed content")} />
                     </div>
                 );
             } else {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 9e85ea28c8..64613d0d88 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1926,6 +1926,8 @@
     "Widgets do not use message encryption.": "Widgets do not use message encryption.",
     "Widget added by": "Widget added by",
     "This widget may use cookies.": "This widget may use cookies.",
+    "Error loading Widget": "Error loading Widget",
+    "Error - Mixed content": "Error - Mixed content",
     "Popout widget": "Popout widget",
     "Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
     "Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages",

From f84ee2276995a5abb42225fa443e8d777dddee65 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 8 Jun 2021 16:36:53 +0100
Subject: [PATCH 089/159] Add comment

---
 src/components/views/right_panel/PinnedMessagesCard.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx
index 3a4dc9cc92..a72731522f 100644
--- a/src/components/views/right_panel/PinnedMessagesCard.tsx
+++ b/src/components/views/right_panel/PinnedMessagesCard.tsx
@@ -160,6 +160,7 @@ const PinnedMessagesCard = ({ room, onClose }: IProps) => {
     } else {
         content = <div className="mx_PinnedMessagesCard_empty">
             <div>
+                { /* XXX: We reuse the classes for simplicity, but deliberately not the components for non-interactivity. */ }
                 <div className="mx_PinnedMessagesCard_MessageActionBar">
                     <div className="mx_MessageActionBar_maskButton mx_MessageActionBar_reactButton" />
                     <div className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton" />

From 93eb9feaa7931700323d8406a9ecd912c3f660d6 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 8 Jun 2021 16:42:58 +0100
Subject: [PATCH 090/159] iterate PR based on feedback

---
 src/components/views/dialogs/InviteDialog.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 557ea416a8..d7f1644d80 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -1249,12 +1249,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         }
     }
 
-    async onLinkClick(e) {
+    private async onLinkClick(e) {
         e.preventDefault();
         selectText(e.target);
     }
 
-    onCopyClick = async e => {
+    private onCopyClick = async e => {
         e.preventDefault();
         const target = e.target; // copy target before we go async and React throws it away
 
@@ -1348,7 +1348,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             goButtonFn = this.startDm;
             extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
                 <span>{ _t("Some suggestions may be hidden for privacy.") }</span>
-                <p>{ _t("If you can’t see who you’re looking for, send them your invite link below.") }</p>
+                <p>{ _t("If you can't see who you’re looking for, send them your invite link below.") }</p>
             </div>;
             const link = makeUserPermalink(MatrixClientPeg.get().getUserId());
             footer = <div className="mx_InviteDialog_footer">

From 2b99afc741a4b4b9c71f9bc454cf1d433b93f564 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 8 Jun 2021 17:05:56 +0100
Subject: [PATCH 091/159] 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 2e8e2b39d4..a5107cba70 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2256,7 +2256,7 @@
     "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",
     "Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.",
-    "If you can’t see who you’re looking for, send them your invite link below.": "If you can’t see who you’re looking for, send them your invite link below.",
+    "If you can't see who you’re looking for, send them your invite link below.": "If you can't see who you’re looking for, send them your invite link below.",
     "Or send invite link": "Or send invite link",
     "Unnamed Space": "Unnamed Space",
     "Invite to %(roomName)s": "Invite to %(roomName)s",

From 90982f9b3fd1345c5af5e66a46986dd11e12dd2a Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 8 Jun 2021 17:31:08 +0100
Subject: [PATCH 092/159] Fix upgrade to element home button in top left menu

---
 src/components/structures/HostSignupAction.tsx | 5 ++++-
 src/components/structures/UserMenu.tsx         | 4 +---
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx
index 769775d549..46e158d6c8 100644
--- a/src/components/structures/HostSignupAction.tsx
+++ b/src/components/structures/HostSignupAction.tsx
@@ -24,13 +24,16 @@ import { HostSignupStore } from "../../stores/HostSignupStore";
 import SdkConfig from "../../SdkConfig";
 import {replaceableComponent} from "../../utils/replaceableComponent";
 
-interface IProps {}
+interface IProps {
+    onClick?(): void;
+}
 
 interface IState {}
 
 @replaceableComponent("structures.HostSignupAction")
 export default class HostSignupAction extends React.PureComponent<IProps, IState> {
     private openDialog = async () => {
+        this.props.onClick?.();
         await HostSignupStore.instance.setHostSignupActive(true);
     }
 
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index fb4829f879..6a449cf1a2 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -366,9 +366,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
                 const mxDomain = MatrixClientPeg.get().getDomain();
                 const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`)));
                 if (!hostSignupConfig.domains || validDomains.length > 0) {
-                    topSection = <div onClick={this.onCloseMenu}>
-                        <HostSignupAction />
-                    </div>;
+                    topSection = <HostSignupAction onClick={this.onCloseMenu} />;
                 }
             }
         }

From 7155c033f23048acab1c864e758bfed2e3344b93 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 8 Jun 2021 19:20:10 +0000
Subject: [PATCH 093/159] Bump trim-newlines from 3.0.0 to 3.0.1

Bumps [trim-newlines](https://github.com/sindresorhus/trim-newlines) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sindresorhus/trim-newlines/releases)
- [Commits](https://github.com/sindresorhus/trim-newlines/commits)

---
updated-dependencies:
- dependency-name: trim-newlines
  dependency-type: indirect
...

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

diff --git a/yarn.lock b/yarn.lock
index 97179550c9..21243f4f58 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7969,9 +7969,9 @@ tree-kill@^1.2.2:
   integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
 
 trim-newlines@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30"
-  integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
+  integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
 
 trough@^1.0.0:
   version "1.0.5"

From dbb8aa47dcbaf0558607e705727c9c8e86bc12a3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 8 Jun 2021 20:08:39 +0000
Subject: [PATCH 094/159] Bump css-what from 5.0.0 to 5.0.1

Bumps [css-what](https://github.com/fb55/css-what) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/fb55/css-what/releases)
- [Commits](https://github.com/fb55/css-what/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: css-what
  dependency-type: indirect
...

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

diff --git a/yarn.lock b/yarn.lock
index 21243f4f58..7c12ac51d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2721,9 +2721,9 @@ css-select@^4.1.2:
     nth-check "^2.0.0"
 
 css-what@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.0.tgz#f0bf4f8bac07582722346ab243f6a35b512cfc47"
-  integrity sha512-qxyKHQvgKwzwDWC/rGbT821eJalfupxYW2qbSJSAtdSTimsr/MlaGONoNLllaUPZWf8QnbcKM/kPVYUQuEKAFA==
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
+  integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
 
 cssesc@^3.0.0:
   version "3.0.0"

From a28f5754c7cd86d7b6d795c8fcfc6a7468c8e1e6 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 8 Jun 2021 21:58:48 -0400
Subject: [PATCH 095/159] Convert TextForEvent to TypeScript

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/{TextForEvent.js => TextForEvent.ts} | 54 +++++++++++++-----------
 1 file changed, 29 insertions(+), 25 deletions(-)
 rename src/{TextForEvent.js => TextForEvent.ts} (94%)

diff --git a/src/TextForEvent.js b/src/TextForEvent.ts
similarity index 94%
rename from src/TextForEvent.js
rename to src/TextForEvent.ts
index 22ce0dd9cf..649c53664e 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.ts
@@ -25,7 +25,7 @@ import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
 // any text to display at all. For this reason they return deferred values
 // to avoid the expense of looking up translations when they're not needed.
 
-function textForMemberEvent(ev) {
+function textForMemberEvent(ev): () => string | null {
     // XXX: SYJS-16 "sender is sometimes null for join messages"
     const senderName = ev.sender ? ev.sender.name : ev.getSender();
     const targetName = ev.target ? ev.target.name : ev.getStateKey();
@@ -107,7 +107,7 @@ function textForMemberEvent(ev) {
     }
 }
 
-function textForTopicEvent(ev) {
+function textForTopicEvent(ev): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
         senderDisplayName,
@@ -115,7 +115,7 @@ function textForTopicEvent(ev) {
     });
 }
 
-function textForRoomNameEvent(ev) {
+function textForRoomNameEvent(ev): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
 
     if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
@@ -134,12 +134,12 @@ function textForRoomNameEvent(ev) {
     });
 }
 
-function textForTombstoneEvent(ev) {
+function textForTombstoneEvent(ev): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
 }
 
-function textForJoinRulesEvent(ev) {
+function textForJoinRulesEvent(ev): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     switch (ev.getContent().join_rule) {
         case "public":
@@ -159,7 +159,7 @@ function textForJoinRulesEvent(ev) {
     }
 }
 
-function textForGuestAccessEvent(ev) {
+function textForGuestAccessEvent(ev): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     switch (ev.getContent().guest_access) {
         case "can_join":
@@ -175,7 +175,7 @@ function textForGuestAccessEvent(ev) {
     }
 }
 
-function textForRelatedGroupsEvent(ev) {
+function textForRelatedGroupsEvent(ev): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     const groups = ev.getContent().groups || [];
     const prevGroups = ev.getPrevContent().groups || [];
@@ -205,7 +205,7 @@ function textForRelatedGroupsEvent(ev) {
     }
 }
 
-function textForServerACLEvent(ev) {
+function textForServerACLEvent(ev): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     const prevContent = ev.getPrevContent();
     const current = ev.getContent();
@@ -235,7 +235,7 @@ function textForServerACLEvent(ev) {
     return getText;
 }
 
-function textForMessageEvent(ev) {
+function textForMessageEvent(ev): () => string | null {
     return () => {
         const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
         let message = senderDisplayName + ': ' + ev.getContent().body;
@@ -248,7 +248,7 @@ function textForMessageEvent(ev) {
     };
 }
 
-function textForCanonicalAliasEvent(ev) {
+function textForCanonicalAliasEvent(ev): () => string | null {
     const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     const oldAlias = ev.getPrevContent().alias;
     const oldAltAliases = ev.getPrevContent().alt_aliases || [];
@@ -299,7 +299,7 @@ function textForCanonicalAliasEvent(ev) {
     });
 }
 
-function textForCallAnswerEvent(event) {
+function textForCallAnswerEvent(event): () => string | null {
     return () => {
         const senderName = event.sender ? event.sender.name : _t('Someone');
         const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
@@ -307,7 +307,7 @@ function textForCallAnswerEvent(event) {
     };
 }
 
-function textForCallHangupEvent(event) {
+function textForCallHangupEvent(event): () => string | null {
     const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
     const eventContent = event.getContent();
     let getReason = () => "";
@@ -344,14 +344,14 @@ function textForCallHangupEvent(event) {
     return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
 }
 
-function textForCallRejectEvent(event) {
+function textForCallRejectEvent(event): () => string | null {
     return () => {
         const senderName = event.sender ? event.sender.name : _t('Someone');
         return _t('%(senderName)s declined the call.', {senderName});
     };
 }
 
-function textForCallInviteEvent(event) {
+function textForCallInviteEvent(event): () => string | null {
     const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
     // FIXME: Find a better way to determine this from the event?
     let isVoice = true;
@@ -383,7 +383,7 @@ function textForCallInviteEvent(event) {
     }
 }
 
-function textForThreePidInviteEvent(event) {
+function textForThreePidInviteEvent(event): () => string | null {
     const senderName = event.sender ? event.sender.name : event.getSender();
 
     if (!isValid3pidInvite(event)) {
@@ -399,7 +399,7 @@ function textForThreePidInviteEvent(event) {
     });
 }
 
-function textForHistoryVisibilityEvent(event) {
+function textForHistoryVisibilityEvent(event): () => string | null {
     const senderName = event.sender ? event.sender.name : event.getSender();
     switch (event.getContent().history_visibility) {
         case 'invited':
@@ -421,7 +421,7 @@ function textForHistoryVisibilityEvent(event) {
 }
 
 // Currently will only display a change if a user's power level is changed
-function textForPowerEvent(event) {
+function textForPowerEvent(event): () => string | null {
     const senderName = event.sender ? event.sender.name : event.getSender();
     if (!event.getPrevContent() || !event.getPrevContent().users ||
         !event.getContent() || !event.getContent().users) {
@@ -466,12 +466,12 @@ function textForPowerEvent(event) {
     });
 }
 
-function textForPinnedEvent(event) {
+function textForPinnedEvent(event): () => string | null {
     const senderName = event.sender ? event.sender.name : event.getSender();
     return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
 }
 
-function textForWidgetEvent(event) {
+function textForWidgetEvent(event): () => string | null {
     const senderName = event.getSender();
     const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
     const {name, type, url} = event.getContent() || {};
@@ -501,12 +501,12 @@ function textForWidgetEvent(event) {
     }
 }
 
-function textForWidgetLayoutEvent(event) {
+function textForWidgetLayoutEvent(event): () => string | null {
     const senderName = event.sender?.name || event.getSender();
     return () => _t("%(senderName)s has updated the widget layout", {senderName});
 }
 
-function textForMjolnirEvent(event) {
+function textForMjolnirEvent(event): () => string | null {
     const senderName = event.getSender();
     const {entity: prevEntity} = event.getPrevContent();
     const {entity, recommendation, reason} = event.getContent();
@@ -593,7 +593,11 @@ function textForMjolnirEvent(event) {
         "for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
 }
 
-const handlers = {
+interface IHandlers {
+    [type: string]: (ev: any) => (() => string | null);
+}
+
+const handlers: IHandlers = {
     'm.room.message': textForMessageEvent,
     'm.call.invite': textForCallInviteEvent,
     'm.call.answer': textForCallAnswerEvent,
@@ -601,7 +605,7 @@ const handlers = {
     'm.call.reject': textForCallRejectEvent,
 };
 
-const stateHandlers = {
+const stateHandlers: IHandlers = {
     'm.room.canonical_alias': textForCanonicalAliasEvent,
     'm.room.name': textForRoomNameEvent,
     'm.room.topic': textForTopicEvent,
@@ -626,12 +630,12 @@ for (const evType of ALL_RULE_TYPES) {
     stateHandlers[evType] = textForMjolnirEvent;
 }
 
-export function hasText(ev) {
+export function hasText(ev): boolean {
     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
     return Boolean(handler?.(ev));
 }
 
-export function textForEvent(ev) {
+export function textForEvent(ev): string {
     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
     return handler?.(ev)?.() || '';
 }

From fccd71f82e9101ebbcab23d3ec04e6521958359f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Wed, 9 Jun 2021 06:23:51 +0200
Subject: [PATCH 096/159] Update styling based on review
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/messages/_SenderProfile.scss | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss
index 378d9e6f1a..7aaafa6ac3 100644
--- a/res/css/views/messages/_SenderProfile.scss
+++ b/res/css/views/messages/_SenderProfile.scss
@@ -20,8 +20,7 @@ limitations under the License.
 
 .mx_SenderProfile_mxid {
     font-weight: 600;
-    font-family: monospace;
-    font-size: 1rem;
+    font-size: 1.1rem;
     color: $event-timestamp-color;
     margin-left: 5px;
 }

From c9b1d1aef06ece68109c46871bf633f823d41c7c Mon Sep 17 00:00:00 2001
From: Germain <germain@souquet.com>
Date: Wed, 9 Jun 2021 10:38:28 +0100
Subject: [PATCH 097/159] Only run e2e tests on develop and PR targetting
 develop

Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
---
 .github/workflows/end-to-end-tests.yml | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml
index 271ea06529..abf7813bbc 100644
--- a/.github/workflows/end-to-end-tests.yml
+++ b/.github/workflows/end-to-end-tests.yml
@@ -1,5 +1,9 @@
 name: "end-to-end_tests"
-on: [push]
+on:
+    push:
+        branches: [develop]
+    pull_request:
+        branches: [develop]
 jobs:
     "end-to-end_tests":
         runs-on: ubuntu-latest

From 85d1bb65c6046330c2fd732621d9e94ced82a072 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 9 Jun 2021 11:40:20 +0100
Subject: [PATCH 098/159] Upgrade to React@17

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

diff --git a/package.json b/package.json
index 13047b69cf..ab855b84be 100644
--- a/package.json
+++ b/package.json
@@ -90,9 +90,9 @@
     "qrcode": "^1.4.4",
     "qs": "^6.9.6",
     "re-resizable": "^6.9.0",
-    "react": "^16.14.0",
+    "react": "^17.0.2",
     "react-beautiful-dnd": "^4.0.1",
-    "react-dom": "^16.14.0",
+    "react-dom": "^17.0.2",
     "react-focus-lock": "^2.5.0",
     "react-transition-group": "^4.4.1",
     "resize-observer-polyfill": "^1.5.1",
@@ -147,7 +147,7 @@
     "chokidar": "^3.5.1",
     "concurrently": "^5.3.0",
     "enzyme": "^3.11.0",
-    "enzyme-adapter-react-16": "^1.15.6",
+    "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
     "eslint": "7.18.0",
     "eslint-config-matrix-org": "^0.2.0",
     "eslint-plugin-babel": "^5.3.1",
@@ -162,7 +162,7 @@
     "matrix-mock-request": "^1.2.3",
     "matrix-react-test-utils": "^0.2.2",
     "matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
-    "react-test-renderer": "^16.14.0",
+    "react-test-renderer": "^17.0.2",
     "rimraf": "^3.0.2",
     "stylelint": "^13.9.0",
     "stylelint-config-standard": "^20.0.0",
diff --git a/yarn.lock b/yarn.lock
index 0ff235a660..32480a156f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1752,6 +1752,31 @@
     "@typescript-eslint/types" "4.14.0"
     eslint-visitor-keys "^2.0.0"
 
+"@wojtekmaj/enzyme-adapter-react-17@^0.6.1":
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.1.tgz#28caa37118c183e5f13c4dfb68cc32cde828ecbc"
+  integrity sha512-xgPfzLVpN0epIHeZofahwr5qwpukEDNAbrufgeDWN6vZPtfblGCC+OZG5TlfK+A6ePVy8sBkD8S2X4tO17JKjg==
+  dependencies:
+    "@wojtekmaj/enzyme-adapter-utils" "^0.1.0"
+    enzyme-shallow-equal "^1.0.0"
+    has "^1.0.0"
+    object.assign "^4.1.0"
+    object.values "^1.1.0"
+    prop-types "^15.7.0"
+    react-is "^17.0.0"
+    react-test-renderer "^17.0.0"
+
+"@wojtekmaj/enzyme-adapter-utils@^0.1.0":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@wojtekmaj/enzyme-adapter-utils/-/enzyme-adapter-utils-0.1.0.tgz#3a2a3db756111d53357e2f119a1612a969ab8c38"
+  integrity sha512-EYK/Vy0Y1ap0jH2UNQjOKtR/7HWkbEq8N+cwC5+yDf+Mwp5uu7j4Qg70RmWuzsA35DGGwgkop6m4pQsGwNOF2A==
+  dependencies:
+    function.prototype.name "^1.1.0"
+    has "^1.0.0"
+    object.assign "^4.1.0"
+    object.fromentries "^2.0.0"
+    prop-types "^15.7.0"
+
 abab@^2.0.3:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
@@ -1780,21 +1805,6 @@ acorn@^7.1.1, acorn@^7.4.0:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
-airbnb-prop-types@^2.16.0:
-  version "2.16.0"
-  resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2"
-  integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==
-  dependencies:
-    array.prototype.find "^2.1.1"
-    function.prototype.name "^1.1.2"
-    is-regex "^1.1.0"
-    object-is "^1.1.2"
-    object.assign "^4.1.0"
-    object.entries "^1.1.2"
-    prop-types "^15.7.2"
-    prop-types-exact "^1.2.0"
-    react-is "^16.13.1"
-
 ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4:
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@@ -1920,14 +1930,6 @@ array-unique@^0.3.2:
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
   integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
 
-array.prototype.find@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/array.prototype.find/-/array.prototype.find-2.1.1.tgz#3baca26108ca7affb08db06bf0be6cb3115a969c"
-  integrity sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA==
-  dependencies:
-    define-properties "^1.1.3"
-    es-abstract "^1.17.4"
-
 array.prototype.flat@^1.2.3:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123"
@@ -3075,35 +3077,7 @@ entities@~2.0:
   resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
   integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
 
-enzyme-adapter-react-16@^1.15.6:
-  version "1.15.6"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz#fd677a658d62661ac5afd7f7f541f141f8085901"
-  integrity sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g==
-  dependencies:
-    enzyme-adapter-utils "^1.14.0"
-    enzyme-shallow-equal "^1.0.4"
-    has "^1.0.3"
-    object.assign "^4.1.2"
-    object.values "^1.1.2"
-    prop-types "^15.7.2"
-    react-is "^16.13.1"
-    react-test-renderer "^16.0.0-0"
-    semver "^5.7.0"
-
-enzyme-adapter-utils@^1.14.0:
-  version "1.14.0"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz#afbb0485e8033aa50c744efb5f5711e64fbf1ad0"
-  integrity sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg==
-  dependencies:
-    airbnb-prop-types "^2.16.0"
-    function.prototype.name "^1.1.3"
-    has "^1.0.3"
-    object.assign "^4.1.2"
-    object.fromentries "^2.0.3"
-    prop-types "^15.7.2"
-    semver "^5.7.1"
-
-enzyme-shallow-equal@^1.0.1, enzyme-shallow-equal@^1.0.4:
+enzyme-shallow-equal@^1.0.0, enzyme-shallow-equal@^1.0.1:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz#b9256cb25a5f430f9bfe073a84808c1d74fced2e"
   integrity sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==
@@ -3146,7 +3120,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.17.0-next.1, es-abstract@^1.17.4:
+es-abstract@^1.17.0-next.1:
   version "1.17.7"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
   integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
@@ -3183,6 +3157,28 @@ es-abstract@^1.18.0-next.1:
     string.prototype.trimend "^1.0.3"
     string.prototype.trimstart "^1.0.3"
 
+es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
+  version "1.18.3"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0"
+  integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==
+  dependencies:
+    call-bind "^1.0.2"
+    es-to-primitive "^1.2.1"
+    function-bind "^1.1.1"
+    get-intrinsic "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.2"
+    is-callable "^1.2.3"
+    is-negative-zero "^2.0.1"
+    is-regex "^1.1.3"
+    is-string "^1.0.6"
+    object-inspect "^1.10.3"
+    object-keys "^1.1.1"
+    object.assign "^4.1.2"
+    string.prototype.trimend "^1.0.4"
+    string.prototype.trimstart "^1.0.4"
+    unbox-primitive "^1.0.1"
+
 es-get-iterator@^1.0.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.1.tgz#b93ddd867af16d5118e00881396533c1c6647ad9"
@@ -3965,7 +3961,17 @@ function-bind@^1.1.1:
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
-function.prototype.name@^1.1.2, function.prototype.name@^1.1.3:
+function.prototype.name@^1.1.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.4.tgz#e4ea839b9d3672ae99d0efd9f38d9191c5eaac83"
+  integrity sha512-iqy1pIotY/RmhdFZygSSlW0wko2yxkSCKqsuv4pr8QESohpYyG/Z7B/XXvPRKTJS//960rgguE5mSRUsDdaJrQ==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    es-abstract "^1.18.0-next.2"
+    functions-have-names "^1.2.2"
+
+function.prototype.name@^1.1.2:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.3.tgz#0bb034bb308e7682826f215eb6b2ae64918847fe"
   integrity sha512-H51qkbNSp8mtkJt+nyW1gyStBiKZxfRqySNUR99ylq6BPXHKI4SEvIlTKp4odLfjRKJV04DFWMU3G/YRlQOsag==
@@ -3980,7 +3986,7 @@ functional-red-black-tree@^1.0.1:
   resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
   integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
 
-functions-have-names@^1.2.0, functions-have-names@^1.2.1:
+functions-have-names@^1.2.0, functions-have-names@^1.2.1, functions-have-names@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
   integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
@@ -4004,6 +4010,15 @@ get-intrinsic@^1.0.1, get-intrinsic@^1.0.2:
     has "^1.0.3"
     has-symbols "^1.0.1"
 
+get-intrinsic@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
+  integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
+  dependencies:
+    function-bind "^1.1.1"
+    has "^1.0.3"
+    has-symbols "^1.0.1"
+
 get-package-type@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@@ -4157,6 +4172,11 @@ hard-rejection@^2.1.0:
   resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
   integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
 
+has-bigints@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
+  integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
+
 has-flag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -4172,6 +4192,11 @@ has-symbols@^1.0.1:
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
   integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
 
+has-symbols@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
+  integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
+
 has-value@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@@ -4203,7 +4228,7 @@ has-values@^1.0.0:
     is-number "^3.0.0"
     kind-of "^4.0.0"
 
-has@^1.0.1, has@^1.0.3:
+has@^1.0.0, has@^1.0.1, has@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
   integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
@@ -4534,6 +4559,11 @@ is-callable@^1.0.4, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.2:
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
   integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
 
+is-callable@^1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
+  integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
+
 is-ci@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
@@ -4736,13 +4766,21 @@ is-potential-custom-element-name@^1.0.0:
   resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
   integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
 
-is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.1:
+is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
   integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
   dependencies:
     has-symbols "^1.0.1"
 
+is-regex@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.3.tgz#d029f9aff6448b93ebbe3f33dac71511fdcbef9f"
+  integrity sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==
+  dependencies:
+    call-bind "^1.0.2"
+    has-symbols "^1.0.2"
+
 is-regexp@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
@@ -4768,6 +4806,11 @@ is-string@^1.0.4, is-string@^1.0.5:
   resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
   integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
 
+is-string@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
+  integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
+
 is-subset@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
@@ -6081,6 +6124,11 @@ object-inspect@^1.1.0, object-inspect@^1.7.0, object-inspect@^1.8.0, object-insp
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
   integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
 
+object-inspect@^1.10.3:
+  version "1.10.3"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
+  integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==
+
 object-is@^1.0.2, object-is@^1.1.2:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068"
@@ -6121,7 +6169,17 @@ object.entries@^1.1.0, object.entries@^1.1.1, object.entries@^1.1.2:
     es-abstract "^1.18.0-next.1"
     has "^1.0.3"
 
-object.fromentries@^2.0.2, object.fromentries@^2.0.3:
+object.fromentries@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.4.tgz#26e1ba5c4571c5c6f0890cef4473066456a120b8"
+  integrity sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    es-abstract "^1.18.0-next.2"
+    has "^1.0.3"
+
+object.fromentries@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.3.tgz#13cefcffa702dc67750314a3305e8cb3fad1d072"
   integrity sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==
@@ -6138,7 +6196,16 @@ object.pick@^1.3.0:
   dependencies:
     isobject "^3.0.1"
 
-object.values@^1.1.1, object.values@^1.1.2:
+object.values@^1.1.0:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
+  integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+    es-abstract "^1.18.2"
+
+object.values@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731"
   integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==
@@ -6588,16 +6655,7 @@ prompts@^2.0.1:
     kleur "^3.0.3"
     sisteransi "^1.0.5"
 
-prop-types-exact@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/prop-types-exact/-/prop-types-exact-1.2.0.tgz#825d6be46094663848237e3925a98c6e944e9869"
-  integrity sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==
-  dependencies:
-    has "^1.0.3"
-    object.assign "^4.1.0"
-    reflect.ownkeys "^0.2.0"
-
-prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2:
   version "15.7.2"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
   integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -6729,15 +6787,14 @@ react-clientside-effect@^1.2.2:
   dependencies:
     "@babel/runtime" "^7.0.0"
 
-react-dom@^16.14.0:
-  version "16.14.0"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"
-  integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==
+react-dom@^17.0.2:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
+  integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
-    prop-types "^15.6.2"
-    scheduler "^0.19.1"
+    scheduler "^0.20.2"
 
 react-focus-lock@^2.5.0:
   version "2.5.0"
@@ -6751,7 +6808,12 @@ react-focus-lock@^2.5.0:
     use-callback-ref "^1.2.1"
     use-sidecar "^1.0.1"
 
-react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
+"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.0, react-is@^17.0.2:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+  integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
+react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -6788,15 +6850,23 @@ react-redux@^5.0.6:
     react-is "^16.6.0"
     react-lifecycles-compat "^3.0.0"
 
-react-test-renderer@^16.0.0-0, react-test-renderer@^16.14.0:
-  version "16.14.0"
-  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.14.0.tgz#e98360087348e260c56d4fe2315e970480c228ae"
-  integrity sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==
+react-shallow-renderer@^16.13.1:
+  version "16.14.1"
+  resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz#bf0d02df8a519a558fd9b8215442efa5c840e124"
+  integrity sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==
   dependencies:
     object-assign "^4.1.1"
-    prop-types "^15.6.2"
-    react-is "^16.8.6"
-    scheduler "^0.19.1"
+    react-is "^16.12.0 || ^17.0.0"
+
+react-test-renderer@^17.0.0, react-test-renderer@^17.0.2:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c"
+  integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==
+  dependencies:
+    object-assign "^4.1.1"
+    react-is "^17.0.2"
+    react-shallow-renderer "^16.13.1"
+    scheduler "^0.20.2"
 
 react-transition-group@^4.4.1:
   version "4.4.1"
@@ -6808,14 +6878,13 @@ react-transition-group@^4.4.1:
     loose-envify "^1.4.0"
     prop-types "^15.6.2"
 
-react@^16.14.0:
-  version "16.14.0"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
-  integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
+react@^17.0.2:
+  version "17.0.2"
+  resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
+  integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
-    prop-types "^15.6.2"
 
 read-pkg-up@^2.0.0:
   version "2.0.0"
@@ -6923,11 +6992,6 @@ redux@^3.7.2:
     loose-envify "^1.1.0"
     symbol-observable "^1.0.3"
 
-reflect.ownkeys@^0.2.0:
-  version "0.2.0"
-  resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
-  integrity sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=
-
 regenerate-unicode-properties@^8.2.0:
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@@ -7266,15 +7330,15 @@ saxes@^5.0.0:
   dependencies:
     xmlchars "^2.2.0"
 
-scheduler@^0.19.1:
-  version "0.19.1"
-  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
-  integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
+scheduler@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
+  integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
   dependencies:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
 
-"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
+"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -7622,6 +7686,14 @@ string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3:
     call-bind "^1.0.0"
     define-properties "^1.1.3"
 
+string.prototype.trimend@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
+  integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+
 string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa"
@@ -7630,6 +7702,14 @@ string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3:
     call-bind "^1.0.0"
     define-properties "^1.1.3"
 
+string.prototype.trimstart@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
+  integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+
 string_decoder@^1.1.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -8078,6 +8158,16 @@ ua-parser-js@^0.7.18:
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
   integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
 
+unbox-primitive@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
+  integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
+  dependencies:
+    function-bind "^1.1.1"
+    has-bigints "^1.0.1"
+    has-symbols "^1.0.2"
+    which-boxed-primitive "^1.0.2"
+
 unhomoglyph@^1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3"
@@ -8352,7 +8442,7 @@ whatwg-url@^8.0.0:
     tr46 "^2.0.2"
     webidl-conversions "^6.1.0"
 
-which-boxed-primitive@^1.0.1:
+which-boxed-primitive@^1.0.1, which-boxed-primitive@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
   integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==

From d492ee4d8acbf0156492cbd97e3f2f3e8153a234 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 9 Jun 2021 11:57:29 +0100
Subject: [PATCH 099/159] Update Enzyme adapter name

---
 test/accessibility/RovingTabIndex-test.js     |  2 +-
 .../structures/MessagePanel-test.js           | 32 +++++++++----------
 .../views/messages/TextualBody-test.js        |  2 +-
 .../views/rooms/SendMessageComposer-test.js   |  2 +-
 4 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/test/accessibility/RovingTabIndex-test.js b/test/accessibility/RovingTabIndex-test.js
index 5aa93f99f3..7af28043e0 100644
--- a/test/accessibility/RovingTabIndex-test.js
+++ b/test/accessibility/RovingTabIndex-test.js
@@ -16,7 +16,7 @@ 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 Adapter from "@wojtekmaj/enzyme-adapter-react-17";
 import { configure, mount } from "enzyme";
 
 import {
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 5b466b4bb0..9426998160 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -32,7 +32,7 @@ import Matrix from 'matrix-js-sdk';
 const test_utils = require('../../test-utils');
 const mockclock = require('../../mock-clock');
 
-import Adapter from "enzyme-adapter-react-16";
+import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
 import { configure, mount } from "enzyme";
 
 import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
@@ -77,7 +77,7 @@ describe('MessagePanel', function() {
         DMRoomMap.makeShared();
     });
 
-    afterEach(function () {
+    afterEach(function() {
         clock.uninstall();
     });
 
@@ -270,7 +270,7 @@ describe('MessagePanel', function() {
 
     it('should show the events', function() {
         const res = TestUtils.renderIntoDocument(
-                <WrappedMessagePanel className="cls" events={events} />,
+            <WrappedMessagePanel className="cls" events={events} />,
         );
 
         // just check we have the right number of tiles for now
@@ -298,8 +298,8 @@ describe('MessagePanel', function() {
 
     it('should insert the read-marker in the right place', function() {
         const res = TestUtils.renderIntoDocument(
-                <WrappedMessagePanel className="cls" events={events} readMarkerEventId={events[4].getId()}
-                    readMarkerVisible={true} />,
+            <WrappedMessagePanel className="cls" events={events} readMarkerEventId={events[4].getId()}
+                readMarkerVisible={true} />,
         );
 
         const tiles = TestUtils.scryRenderedComponentsWithType(
@@ -316,8 +316,8 @@ describe('MessagePanel', function() {
     it('should show the read-marker that fall in summarised events after the summary', function() {
         const melsEvents = mkMelsEvents();
         const res = TestUtils.renderIntoDocument(
-                <WrappedMessagePanel className="cls" events={melsEvents} readMarkerEventId={melsEvents[4].getId()}
-                    readMarkerVisible={true} />,
+            <WrappedMessagePanel className="cls" events={melsEvents} readMarkerEventId={melsEvents[4].getId()}
+                readMarkerVisible={true} />,
         );
 
         const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary');
@@ -334,8 +334,8 @@ describe('MessagePanel', function() {
     it('should hide the read-marker at the end of summarised events', function() {
         const melsEvents = mkMelsEventsOnly();
         const res = TestUtils.renderIntoDocument(
-                <WrappedMessagePanel className="cls" events={melsEvents} readMarkerEventId={melsEvents[9].getId()}
-                    readMarkerVisible={true} />,
+            <WrappedMessagePanel className="cls" events={melsEvents} readMarkerEventId={melsEvents[9].getId()}
+                readMarkerVisible={true} />,
         );
 
         const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary');
@@ -358,9 +358,9 @@ describe('MessagePanel', function() {
 
         // first render with the RM in one place
         let mp = ReactDOM.render(
-                <WrappedMessagePanel className="cls" events={events} readMarkerEventId={events[4].getId()}
-                    readMarkerVisible={true}
-                />, parentDiv);
+            <WrappedMessagePanel className="cls" events={events} readMarkerEventId={events[4].getId()}
+                readMarkerVisible={true}
+            />, parentDiv);
 
         const tiles = TestUtils.scryRenderedComponentsWithType(
             mp, sdk.getComponent('rooms.EventTile'));
@@ -374,9 +374,9 @@ describe('MessagePanel', function() {
 
         // now move the RM
         mp = ReactDOM.render(
-                <WrappedMessagePanel className="cls" events={events} readMarkerEventId={events[6].getId()}
-                    readMarkerVisible={true}
-                />, parentDiv);
+            <WrappedMessagePanel className="cls" events={events} readMarkerEventId={events[6].getId()}
+                readMarkerVisible={true}
+            />, parentDiv);
 
         // now there should be two RM containers
         const found = TestUtils.scryRenderedDOMComponentsWithClass(mp, 'mx_RoomView_myReadMarker_container');
@@ -451,7 +451,7 @@ describe('MessagePanel', function() {
         expect(isReadMarkerVisible(rm)).toBeFalsy();
     });
 
-    it('should render Date separators for the events', function () {
+    it('should render Date separators for the events', function() {
         const events = mkOneDayEvents();
         const res = mount(
             <WrappedMessagePanel
diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js
index 0a6d47a72b..b81486b293 100644
--- a/test/components/views/messages/TextualBody-test.js
+++ b/test/components/views/messages/TextualBody-test.js
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import React from "react";
-import Adapter from "enzyme-adapter-react-16";
+import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
 import { configure, mount } from "enzyme";
 
 import sdk from "../../../skinned-sdk";
diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js
index 64a90eee81..eb7b2ffeff 100644
--- a/test/components/views/rooms/SendMessageComposer-test.js
+++ b/test/components/views/rooms/SendMessageComposer-test.js
@@ -15,7 +15,7 @@ limitations under the License.
 */
 
 import '../../../skinned-sdk'; // Must be first for skinning to work
-import Adapter from "enzyme-adapter-react-16";
+import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
 import { configure, mount } from "enzyme";
 import React from "react";
 import {act} from "react-dom/test-utils";

From 90cc26c78f543f9317bb418c3596c0878b678de5 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 9 Jun 2021 12:17:12 +0100
Subject: [PATCH 100/159] rename workflow

---
 .github/workflows/{end-to-end-tests.yml => develop.yml} | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
 rename .github/workflows/{end-to-end-tests.yml => develop.yml} (96%)

diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/develop.yml
similarity index 96%
rename from .github/workflows/end-to-end-tests.yml
rename to .github/workflows/develop.yml
index abf7813bbc..346dcbaaba 100644
--- a/.github/workflows/end-to-end-tests.yml
+++ b/.github/workflows/develop.yml
@@ -1,11 +1,11 @@
-name: "end-to-end_tests"
+name: Develop jobs
 on:
     push:
         branches: [develop]
     pull_request:
         branches: [develop]
 jobs:
-    "end-to-end_tests":
+    end-to-end:
         runs-on: ubuntu-latest
         defaults:
             run:

From 4e14c612b78eec426ce88e4b9f880f9535d66365 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 9 Jun 2021 12:17:49 +0100
Subject: [PATCH 101/159] Remove extraenous instructions

---
 .github/workflows/develop.yml | 11 +----------
 1 file changed, 1 insertion(+), 10 deletions(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index 346dcbaaba..3f82e61280 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -7,16 +7,7 @@ on:
 jobs:
     end-to-end:
         runs-on: ubuntu-latest
-        defaults:
-            run:
-                shell: bash
-                working-directory: ${{ github.workspace }}
-        container:
-            image: vectorim/element-web-ci-e2etests-env:latest
-            env:
-                CI_PACKAGE: true
-                GITHUB_HEAD_REF: ${{ github.head_ref }}
-                GITHUB_BASE_REF: ${{ github.base_ref }}
+        container: vectorim/element-web-ci-e2etests-env:latest
         steps:
             - name: Checkout code
               uses: actions/checkout@v2

From 15132074e2bd899e55da8be97650e129a05a5b64 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 9 Jun 2021 13:44:31 +0100
Subject: [PATCH 102/159] Restore copy button icon when sharing permalink

---
 res/css/views/dialogs/_ShareDialog.scss | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss
index ce3fdd021f..4d5e1409db 100644
--- a/res/css/views/dialogs/_ShareDialog.scss
+++ b/res/css/views/dialogs/_ShareDialog.scss
@@ -50,7 +50,8 @@ limitations under the License.
     margin-left: 20px;
     display: inherit;
 }
-.mx_ShareDialog_matrixto_copy > div {
+.mx_ShareDialog_matrixto_copy::after {
+    content: "";
     mask-image: url($copy-button-url);
     background-color: $message-action-bar-fg-color;
     margin-left: 5px;

From ceed6ecbe87f743ddd87252a673b16fd6a7bf96a Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 9 Jun 2021 14:03:31 +0100
Subject: [PATCH 103/159] Restore Page Up/Down key bindings when focusing the
 composer

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

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 463b71922f..fe90d2f873 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1669,6 +1669,24 @@ export default class RoomView extends React.Component<IProps, IState> {
         });
     };
 
+    /**
+     * called by the parent component when PageUp/Down/etc is pressed.
+     *
+     * We pass it down to the scroll panel.
+     */
+    private handleScrollKey = ev => {
+        let panel;
+        if (this.searchResultsPanel.current) {
+            panel = this.searchResultsPanel.current;
+        } else if (this.messagePanel) {
+            panel = this.messagePanel;
+        }
+
+        if (panel) {
+            panel.handleScrollKey(ev);
+        }
+    };
+
     /**
      * get any current call for this room
      */

From 927de02a8e8e7a86eafeac59aecc779fecb9e986 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 9 Jun 2021 14:59:55 +0100
Subject: [PATCH 104/159] Revert refreshStickyHeaders optimisations

---
 src/components/structures/LeftPanel.tsx | 1 +
 src/components/views/rooms/RoomList.tsx | 5 ++++-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index 5b6b9c3717..af22db1350 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -439,6 +439,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
             onBlur={this.onBlur}
             isMinimized={this.props.isMinimized}
             activeSpace={this.state.activeSpace}
+            onResize={this.refreshStickyHeaders}
             onListCollapse={this.refreshStickyHeaders}
         />;
 
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 8b36341ed0..5a1c3a24b3 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -55,6 +55,7 @@ interface IProps {
     onKeyDown: (ev: React.KeyboardEvent) => void;
     onFocus: (ev: React.FocusEvent) => void;
     onBlur: (ev: React.FocusEvent) => void;
+    onResize: () => void;
     onListCollapse?: (isExpanded: boolean) => void;
     resizeNotifier: ResizeNotifier;
     isMinimized: boolean;
@@ -404,7 +405,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
             const newSublists = objectWithOnly(newLists, newListIds);
             const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
 
-            this.setState({sublists, isNameFiltering});
+            this.setState({sublists, isNameFiltering}, () => {
+                this.props.onResize();
+            });
         }
     };
 

From b43315c6c1c555ebce7a032dcc2f7950e999ed83 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 9 Jun 2021 15:46:10 +0100
Subject: [PATCH 105/159] Restore read receipt animation from event to event

This restores expected read receipt animation by always including the positioned
parent of read receipts. I imagine there's something smarter we could be doing,
but for now, at least least get back to expected behaviour.

Fixes https://github.com/vector-im/element-web/issues/17561
---
 src/components/views/rooms/EventTile.tsx | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 9e9d7ef53d..354e84bcae 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -644,7 +644,18 @@ export default class EventTile extends React.Component<IProps, IState> {
 
         // return early if there are no read receipts
         if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
-            return null;
+            // We currently must include `mx_EventTile_readAvatars` in the DOM
+            // of all events, as it is the positioned parent of the animated
+            // read receipts. We can't let it unmount when a receipt moves
+            // events, so for now we mount it for all events. With out it, the
+            // animation will start from the top of the timeline (because it
+            // lost its container).
+            // See also https://github.com/vector-im/element-web/issues/17561
+            return (
+                <div className="mx_EventTile_msgOption">
+                    <span className="mx_EventTile_readAvatars" />
+                </div>
+            );
         }
 
         const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');

From e566704bdf1baf5abc81601401d6f00422117923 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 9 Jun 2021 15:48:30 +0100
Subject: [PATCH 106/159] Remove redundant early return

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

diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 354e84bcae..48118444a5 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -663,11 +663,7 @@ export default class EventTile extends React.Component<IProps, IState> {
         const receiptOffset = 15;
         let left = 0;
 
-        const receipts = this.props.readReceipts || [];
-
-        if (receipts.length === 0) {
-            return null;
-        }
+        const receipts = this.props.readReceipts;
 
         for (let i = 0; i < receipts.length; ++i) {
             const receipt = receipts[i];

From c6972c4535c09e914a7aaa2c980c5e2f3fe78eb3 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 9 Jun 2021 15:53:38 +0100
Subject: [PATCH 107/159] Fix spelling

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

diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 48118444a5..0861f7fd5d 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -647,7 +647,7 @@ export default class EventTile extends React.Component<IProps, IState> {
             // We currently must include `mx_EventTile_readAvatars` in the DOM
             // of all events, as it is the positioned parent of the animated
             // read receipts. We can't let it unmount when a receipt moves
-            // events, so for now we mount it for all events. With out it, the
+            // events, so for now we mount it for all events. Without it, the
             // animation will start from the top of the timeline (because it
             // lost its container).
             // See also https://github.com/vector-im/element-web/issues/17561

From bd8fd246c67a4b79f7374e1cad18001080ff4741 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Wed, 9 Jun 2021 16:04:03 +0100
Subject: [PATCH 108/159] Add logging for which rooms calls are in

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

diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 0d87451b5f..bf7cb3473d 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -542,6 +542,7 @@ export default class CallHandler extends EventEmitter {
                 if (newMappedRoomId !== mappedRoomId) {
                     this.removeCallForRoom(mappedRoomId);
                     mappedRoomId = newMappedRoomId;
+                    console.log("Moving call to room " + mappedRoomId);
                     this.calls.set(mappedRoomId, call);
                     this.emit(CallHandlerEvent.CallChangeRoom, call);
                 }
@@ -607,6 +608,7 @@ export default class CallHandler extends EventEmitter {
     }
 
     private removeCallForRoom(roomId: string) {
+        console.log("Removing call for room ", roomId);
         this.calls.delete(roomId);
         this.emit(CallHandlerEvent.CallsChanged, this.calls);
     }
@@ -680,6 +682,7 @@ export default class CallHandler extends EventEmitter {
         console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
         const call = MatrixClientPeg.get().createCall(mappedRoomId);
 
+        console.log("Adding call for room ", roomId);
         this.calls.set(roomId, call);
         this.emit(CallHandlerEvent.CallsChanged, this.calls);
         if (transferee) {
@@ -812,6 +815,7 @@ export default class CallHandler extends EventEmitter {
                     }
 
                     Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
+                    console.log("Adding call for room ", mappedRoomId);
                     this.calls.set(mappedRoomId, call)
                     this.emit(CallHandlerEvent.CallsChanged, this.calls);
                     this.setCallListeners(call);

From b4cb275ca1e3e5b5d7d068b8cacfed85ee375ba7 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 9 Jun 2021 17:59:41 +0100
Subject: [PATCH 109/159] Fix expanding last collapsed sticky session when
 zoomed in

---
 src/components/views/rooms/RoomSublist.tsx | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index 0bb7381dbc..ba8bbffbcc 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -454,8 +454,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
         const sublist = possibleSticky.parentElement.parentElement;
         const list = sublist.parentElement.parentElement;
         // the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky
-        const isAtTop = list.scrollTop <= HEADER_HEIGHT;
-        const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight;
+        const listScrollTop = Math.round(list.scrollTop);
+        const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT);
+        const isAtBottom = listScrollTop >= Math.round(list.scrollHeight - list.offsetHeight);
         const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
         const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');
 

From 4f649f290cafbe49c9bc7ed8ac8c20e1ae77ab35 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Thu, 10 Jun 2021 10:46:21 +0100
Subject: [PATCH 110/159] Migrate RoomSettingsDialog to TypeScript

---
 ...ttingsDialog.js => RoomSettingsDialog.tsx} | 33 ++++++++++---------
 1 file changed, 18 insertions(+), 15 deletions(-)
 rename src/components/views/dialogs/{RoomSettingsDialog.js => RoomSettingsDialog.tsx} (87%)

diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.tsx
similarity index 87%
rename from src/components/views/dialogs/RoomSettingsDialog.js
rename to src/components/views/dialogs/RoomSettingsDialog.tsx
index b6c4d42243..9d1608c9e4 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.js
+++ b/src/components/views/dialogs/RoomSettingsDialog.tsx
@@ -16,7 +16,6 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import TabbedView, {Tab} from "../../structures/TabbedView";
 import {_t, _td} from "../../../languageHandler";
 import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
@@ -39,31 +38,35 @@ export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
 export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
 export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
 
+interface IProps {
+    roomId: string;
+    onFinished: (success: boolean) => void;
+}
+
 @replaceableComponent("views.dialogs.RoomSettingsDialog")
-export default class RoomSettingsDialog extends React.Component {
-    static propTypes = {
-        roomId: PropTypes.string.isRequired,
-        onFinished: PropTypes.func.isRequired,
-    };
+export default class RoomSettingsDialog extends React.Component<IProps> {
+    private dispatcherRef: string;
 
-    componentDidMount() {
-        this._dispatcherRef = dis.register(this._onAction);
+    public componentDidMount() {
+        this.dispatcherRef = dis.register(this.onAction);
     }
 
-    componentWillUnmount() {
-        if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
+    public componentWillUnmount() {
+        if (this.dispatcherRef) {
+            dis.unregister(this.dispatcherRef);
+        }
     }
 
-    _onAction = (payload) => {
+    private onAction = (payload): void => {
         // When view changes below us, close the room settings
         // whilst the modal is open this can only be triggered when someone hits Leave Room
         if (payload.action === 'view_home_page') {
-            this.props.onFinished();
+            this.props.onFinished(true);
         }
     };
 
-    _getTabs() {
-        const tabs = [];
+    private getTabs(): Tab[] {
+        const tabs: Tab[] = [];
 
         tabs.push(new Tab(
             ROOM_GENERAL_TAB,
@@ -123,7 +126,7 @@ export default class RoomSettingsDialog extends React.Component {
                 title={_t("Room Settings - %(roomName)s", {roomName})}
             >
                 <div className='mx_SettingsDialog_content'>
-                    <TabbedView tabs={this._getTabs()} />
+                    <TabbedView tabs={this.getTabs()} />
                 </div>
             </BaseDialog>
         );

From 7f3173f17085ee598e78c20f94db916b7f297fa7 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Thu, 10 Jun 2021 11:14:43 +0100
Subject: [PATCH 111/159] Implement unencrypted warning slate in rooms

---
 .../views/dialogs/RoomSettingsDialog.tsx      |  6 ++-
 .../views/messages/EventTileBubble.tsx        |  3 +-
 src/components/views/rooms/NewRoomIntro.tsx   | 37 +++++++++++++------
 src/i18n/strings/en_EN.json                   |  3 +-
 src/stores/RoomViewStore.tsx                  |  1 +
 5 files changed, 35 insertions(+), 15 deletions(-)

diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx
index 9d1608c9e4..1a664951c5 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.tsx
+++ b/src/components/views/dialogs/RoomSettingsDialog.tsx
@@ -41,6 +41,7 @@ export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
 interface IProps {
     roomId: string;
     onFinished: (success: boolean) => void;
+    initialTabId?: string;
 }
 
 @replaceableComponent("views.dialogs.RoomSettingsDialog")
@@ -126,7 +127,10 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
                 title={_t("Room Settings - %(roomName)s", {roomName})}
             >
                 <div className='mx_SettingsDialog_content'>
-                    <TabbedView tabs={this.getTabs()} />
+                    <TabbedView
+                        tabs={this.getTabs()}
+                        initialTabId={this.props.initialTabId}
+                    />
                 </div>
             </BaseDialog>
         );
diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx
index f797a97a3d..88913ac2d4 100644
--- a/src/components/views/messages/EventTileBubble.tsx
+++ b/src/components/views/messages/EventTileBubble.tsx
@@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {forwardRef, ReactNode} from "react";
+import React, {forwardRef, ReactNode, ReactChildren} from "react";
 import classNames from "classnames";
 
 interface IProps {
     className: string;
     title: string;
     subtitle?: ReactNode;
+    children?: ReactChildren;
 }
 
 const EventTileBubble = forwardRef<HTMLDivElement, IProps>(({ className, title, subtitle, children }, ref) => {
diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 9b003ade89..21282c415a 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -33,6 +33,9 @@ import {showSpaceInvite} from "../../../utils/space";
 
 import { privateShouldBeEncrypted } from "../../../createRoom";
 
+import EventTileBubble from "../messages/EventTileBubble";
+import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
+
 function hasExpectedEncryptionSettings(room): boolean {
     const isEncrypted: boolean = room._client.isRoomEncrypted(room.roomId);
     const isPublic: boolean = room.getJoinRule() === "public";
@@ -174,20 +177,30 @@ const NewRoomIntro = () => {
         </React.Fragment>;
     }
 
+    function openRoomSettings(event) {
+        event.preventDefault();
+        dis.dispatch({
+            action: "open_room_settings",
+            initial_tab_id: ROOM_SECURITY_TAB,
+        });
+    }
+
+    const sub2 = _t("Your private messages are normally encrypted, but this room isn't." +
+    "Usually this is because the room was created with a device or method that doesn't support " +
+    "encryption, like email invites. <a>Enable encryption in settings.</a>", {},
+    { a: sub => <a onClick={openRoomSettings} href="#">{sub}</a> });
+
     return <div className="mx_NewRoomIntro">
+
+        { !hasExpectedEncryptionSettings(room) && (
+            <EventTileBubble
+                className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
+                title={_t("This room isn't end to end encrypted")}
+                subtitle={sub2}
+            />
+        )}
+
         { body }
-
-        { !hasExpectedEncryptionSettings(room) && <p className="mx_NewRoomIntro_message" role="alert">
-            {_t("Messages in this room are not end-to-end encrypted. " +
-                    "Messages sent in an unencrypted room can be seen by the server and third parties. " +
-                    "<a>Learn more about encryption.</a>", {},
-            {
-                a: sub => <a href="https://element.io/help#encryption"
-                    rel="noreferrer noopener" target="_blank"
-                >{sub}</a>,
-            })}
-
-        </p>}
     </div>;
 };
 
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 8508d55eba..3ab69cdb11 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1509,7 +1509,8 @@
     "Invite to just this room": "Invite to just this room",
     "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
     "This is the start of <roomName/>.": "This is the start of <roomName/>.",
-    "Messages in this room are not end-to-end encrypted. Messages sent in an unencrypted room can be seen by the server and third parties. <a>Learn more about encryption.</a>": "Messages in this room are not end-to-end encrypted. Messages sent in an unencrypted room can be seen by the server and third parties. <a>Learn more about encryption.</a>",
+    "Your private messages are normally encrypted, but this room isn't.Usually this is because the room was created with a device or method that doesn't support encryption, like email invites. <a>Enable encryption in settings.</a>": "Your private messages are normally encrypted, but this room isn't.Usually this is because the room was created with a device or method that doesn't support encryption, like email invites. <a>Enable encryption in settings.</a>",
+    "This room isn't end to end encrypted": "This room isn't end to end encrypted",
     "Unpin": "Unpin",
     "View message": "View message",
     "%(duration)ss": "%(duration)ss",
diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx
index 8da565f603..cc3eafffcd 100644
--- a/src/stores/RoomViewStore.tsx
+++ b/src/stores/RoomViewStore.tsx
@@ -167,6 +167,7 @@ class RoomViewStore extends Store<ActionPayload> {
                 const RoomSettingsDialog = sdk.getComponent("dialogs.RoomSettingsDialog");
                 Modal.createTrackedDialog('Room settings', '', RoomSettingsDialog, {
                     roomId: payload.room_id || this.state.roomId,
+                    initialTabId: payload.initial_tab_id,
                 }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
                 break;
             }

From 0e2e327c4650d835abf8b2b0d9cee39982383144 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Thu, 10 Jun 2021 11:19:25 +0100
Subject: [PATCH 112/159] Delete new room intro stale CSS

---
 res/css/views/rooms/_NewRoomIntro.scss | 20 --------------------
 1 file changed, 20 deletions(-)

diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss
index 357273b597..e0cccfa885 100644
--- a/res/css/views/rooms/_NewRoomIntro.scss
+++ b/res/css/views/rooms/_NewRoomIntro.scss
@@ -69,24 +69,4 @@ limitations under the License.
         font-size: $font-15px;
         color: $secondary-fg-color;
     }
-
-    .mx_NewRoomIntro_message:not(:first-child) {
-        padding-top: 1em;
-        margin-top: 1em;
-        border-top: 1px solid currentColor;
-    }
-
-    .mx_NewRoomIntro_message[role=alert]::before {
-        --size: 25px;
-        content: "!";
-        float: left;
-        border-radius: 50%;
-        width: var(--size);
-        height: var(--size);
-        line-height: var(--size);
-        text-align: center;
-        background: $button-danger-bg-color;
-        color: #fff;
-        margin-right: calc(var(--size) / 2);
-    }
 }

From 6606a1142ded38fe85953d1cc881599491aa3054 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Thu, 10 Jun 2021 11:29:10 +0100
Subject: [PATCH 113/159] Update unencrypted room warning copy

---
 src/components/views/rooms/NewRoomIntro.tsx | 8 ++++----
 src/i18n/strings/en_EN.json                 | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 21282c415a..76240f6a2f 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -185,9 +185,9 @@ const NewRoomIntro = () => {
         });
     }
 
-    const sub2 = _t("Your private messages are normally encrypted, but this room isn't." +
-    "Usually this is because the room was created with a device or method that doesn't support " +
-    "encryption, like email invites. <a>Enable encryption in settings.</a>", {},
+    const sub2 = _t("Your private messages are normally encrypted, but this room isn't. "+
+    "Usually this is due to an unsupported device or method being used, " +
+    "like email invites. <a>Enable encryption in settings.</a>", {},
     { a: sub => <a onClick={openRoomSettings} href="#">{sub}</a> });
 
     return <div className="mx_NewRoomIntro">
@@ -195,7 +195,7 @@ const NewRoomIntro = () => {
         { !hasExpectedEncryptionSettings(room) && (
             <EventTileBubble
                 className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
-                title={_t("This room isn't end to end encrypted")}
+                title={_t("End-to-end encryption isn't enabled")}
                 subtitle={sub2}
             />
         )}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 3ab69cdb11..874dc11bd2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1509,8 +1509,8 @@
     "Invite to just this room": "Invite to just this room",
     "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
     "This is the start of <roomName/>.": "This is the start of <roomName/>.",
-    "Your private messages are normally encrypted, but this room isn't.Usually this is because the room was created with a device or method that doesn't support encryption, like email invites. <a>Enable encryption in settings.</a>": "Your private messages are normally encrypted, but this room isn't.Usually this is because the room was created with a device or method that doesn't support encryption, like email invites. <a>Enable encryption in settings.</a>",
-    "This room isn't end to end encrypted": "This room isn't end to end encrypted",
+    "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. <a>Enable encryption in settings.</a>": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. <a>Enable encryption in settings.</a>",
+    "End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled",
     "Unpin": "Unpin",
     "View message": "View message",
     "%(duration)ss": "%(duration)ss",

From 9731e275f8665b59b8a50746603ad4754392848d Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Thu, 10 Jun 2021 12:15:55 +0100
Subject: [PATCH 114/159] cater for an undefined MatrixClient on rooms

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

diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 76240f6a2f..390a570b26 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -37,7 +37,7 @@ import EventTileBubble from "../messages/EventTileBubble";
 import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
 
 function hasExpectedEncryptionSettings(room): boolean {
-    const isEncrypted: boolean = room._client.isRoomEncrypted(room.roomId);
+    const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId);
     const isPublic: boolean = room.getJoinRule() === "public";
     return isPublic || !privateShouldBeEncrypted() || isEncrypted;
 }

From 79b8fbc2a909364972043ebe06019406247fb9b5 Mon Sep 17 00:00:00 2001
From: Germain <germain@souquet.com>
Date: Thu, 10 Jun 2021 14:21:48 +0100
Subject: [PATCH 115/159] Add indentation for unencrypted warning text

Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
---
 src/components/views/rooms/NewRoomIntro.tsx | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 390a570b26..3bf9a9db33 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -185,10 +185,12 @@ const NewRoomIntro = () => {
         });
     }
 
-    const sub2 = _t("Your private messages are normally encrypted, but this room isn't. "+
-    "Usually this is due to an unsupported device or method being used, " +
-    "like email invites. <a>Enable encryption in settings.</a>", {},
-    { a: sub => <a onClick={openRoomSettings} href="#">{sub}</a> });
+    const sub2 = _t(
+        "Your private messages are normally encrypted, but this room isn't. "+
+        "Usually this is due to an unsupported device or method being used, " +
+        "like email invites. <a>Enable encryption in settings.</a>", {},
+        { a: sub => <a onClick={openRoomSettings} href="#">{sub}</a> },
+    );
 
     return <div className="mx_NewRoomIntro">
 

From 8174973cfed821249db3c3db61a489c79d776039 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 10 Jun 2021 15:31:27 +0200
Subject: [PATCH 116/159] Use the same styling as for mx_TextualEvent
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/messages/_SenderProfile.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss
index 7aaafa6ac3..08644b14e3 100644
--- a/res/css/views/messages/_SenderProfile.scss
+++ b/res/css/views/messages/_SenderProfile.scss
@@ -21,6 +21,6 @@ limitations under the License.
 .mx_SenderProfile_mxid {
     font-weight: 600;
     font-size: 1.1rem;
-    color: $event-timestamp-color;
     margin-left: 5px;
+    opacity: 0.5; // Match mx_TextualEvent
 }

From 5343be7814e9fd120c3779aa8a33951716cb2d0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Thu, 10 Jun 2021 18:53:56 +0200
Subject: [PATCH 117/159] Fix buggy hovering/selecting of event tiles
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   | 18 +++++++++++-------
 res/css/views/rooms/_GroupLayout.scss |  8 --------
 2 files changed, 11 insertions(+), 15 deletions(-)

diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index c8b4138f27..3af266caee 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -16,6 +16,7 @@ limitations under the License.
 */
 
 $left-gutter: 64px;
+$hover-select-border: 4px;
 
 .mx_EventTile {
     max-width: 100%;
@@ -142,8 +143,7 @@ $left-gutter: 64px;
 }
 
 .mx_EventTile_selected > div > a > .mx_MessageTimestamp {
-    left: 3px;
-    width: auto;
+    left: calc(-$hover-select-border);
 }
 
 .mx_EventTile:hover .mx_MessageActionBar,
@@ -158,7 +158,7 @@ $left-gutter: 64px;
  */
 .mx_EventTile_selected > .mx_EventTile_line {
     border-left: $accent-color 4px solid;
-    padding-left: 60px;
+    padding-left: calc($left-gutter - $hover-select-border);
     background-color: $event-selected-color;
 }
 
@@ -171,8 +171,12 @@ $left-gutter: 64px;
     }
 }
 
+.mx_EventTile_info .mx_EventTile_line {
+    padding-left: calc($left-gutter + 18px);
+}
+
 .mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
-    padding-left: 78px;
+    padding-left: calc($left-gutter + 18px - $hover-select-border);
 }
 
 .mx_EventTile:hover .mx_EventTile_line,
@@ -408,7 +412,7 @@ $left-gutter: 64px;
 .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
 .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
 .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
-    padding-left: 60px;
+    padding-left: calc($left-gutter - $hover-select-border);
 }
 
 .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
@@ -426,7 +430,7 @@ $left-gutter: 64px;
 .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
 .mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
 .mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
-    padding-left: 78px;
+    padding-left: calc($left-gutter + 18px - $hover-select-border);
 }
 
 /* End to end encryption stuff */
@@ -438,7 +442,7 @@ $left-gutter: 64px;
 .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
 .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
 .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
-    width: $MessageTimestamp_width_hover;
+    left: calc(-$hover-select-border);
 }
 
 // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss
index 818509785b..ddee81a914 100644
--- a/res/css/views/rooms/_GroupLayout.scss
+++ b/res/css/views/rooms/_GroupLayout.scss
@@ -24,10 +24,6 @@ $left-gutter: 64px;
             margin-left: $left-gutter;
         }
 
-        > .mx_EventTile_line {
-            padding-left: $left-gutter;
-        }
-
         > .mx_EventTile_avatar {
             position: absolute;
         }
@@ -43,10 +39,6 @@ $left-gutter: 64px;
             line-height: $font-22px;
         }
     }
-
-    .mx_EventTile_info .mx_EventTile_line {
-        padding-left: calc($left-gutter + 18px);
-    }
 }
 
 /* Compact layout overrides */

From b1824ce579c805c8c293aee739808b71bc030614 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Fri, 11 Jun 2021 11:34:53 +0100
Subject: [PATCH 118/159] fix HTML tag for Event Tile when not rendered in a
 list

---
 .../views/elements/EventTilePreview.tsx       |   1 +
 src/components/views/rooms/EventTile.tsx      | 150 ++++++++++--------
 src/components/views/rooms/ReplyPreview.js    |   1 +
 3 files changed, 82 insertions(+), 70 deletions(-)

diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index b15fbbed2b..cf3b7a6e61 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -128,6 +128,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
                 mxEvent={event}
                 layout={this.props.layout}
                 enableFlair={SettingsStore.getValue(UIFeature.Flair)}
+                as="div"
             />
         </div>;
     }
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 0861f7fd5d..85b9cac2c4 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -1059,58 +1059,65 @@ export default class EventTile extends React.Component<IProps, IState> {
         switch (this.props.tileShape) {
             case 'notif': {
                 const room = this.context.getRoom(this.props.mxEvent.getRoomId());
-                return (
-                    <li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
-                        <div className="mx_EventTile_roomName">
-                            <RoomAvatar room={room} width={28} height={28} />
-                            <a href={permalink} onClick={this.onPermalinkClicked}>
-                                { room ? room.name : '' }
-                            </a>
-                        </div>
-                        <div className="mx_EventTile_senderDetails">
-                            { avatar }
-                            <a href={permalink} onClick={this.onPermalinkClicked}>
-                                { sender }
-                                { timestamp }
-                            </a>
-                        </div>
-                        <div className="mx_EventTile_line">
-                            <EventTileType ref={this.tile}
-                                mxEvent={this.props.mxEvent}
-                                highlights={this.props.highlights}
-                                highlightLink={this.props.highlightLink}
-                                showUrlPreview={this.props.showUrlPreview}
-                                onHeightChanged={this.props.onHeightChanged}
-                            />
-                        </div>
-                    </li>
-                );
+                return React.createElement(this.props.as || "li", {
+                    "className": classes,
+                    "aria-live": ariaLive,
+                    "aria-atomic": true,
+                    "data-scroll-tokens": scrollToken,
+                }, [
+                    <div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
+                        <RoomAvatar room={room} width={28} height={28} />
+                        <a href={permalink} onClick={this.onPermalinkClicked}>
+                            { room ? room.name : '' }
+                        </a>
+                    </div>,
+                    <div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
+                        { avatar }
+                        <a href={permalink} onClick={this.onPermalinkClicked}>
+                            { sender }
+                            { timestamp }
+                        </a>
+                    </div>,
+                    <div className="mx_EventTile_line" key="mx_EventTile_line">
+                        <EventTileType ref={this.tile}
+                            mxEvent={this.props.mxEvent}
+                            highlights={this.props.highlights}
+                            highlightLink={this.props.highlightLink}
+                            showUrlPreview={this.props.showUrlPreview}
+                            onHeightChanged={this.props.onHeightChanged}
+                        />
+                    </div>,
+                ]);
             }
             case 'file_grid': {
-                return (
-                    <li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
-                        <div className="mx_EventTile_line">
-                            <EventTileType ref={this.tile}
-                                mxEvent={this.props.mxEvent}
-                                highlights={this.props.highlights}
-                                highlightLink={this.props.highlightLink}
-                                showUrlPreview={this.props.showUrlPreview}
-                                tileShape={this.props.tileShape}
-                                onHeightChanged={this.props.onHeightChanged}
-                            />
+                return React.createElement(this.props.as || "li", {
+                    "className": classes,
+                    "aria-live": ariaLive,
+                    "aria-atomic": true,
+                    "data-scroll-tokens": scrollToken,
+                }, [
+                    <div className="mx_EventTile_line" key="mx_EventTile_line">
+                        <EventTileType ref={this.tile}
+                            mxEvent={this.props.mxEvent}
+                            highlights={this.props.highlights}
+                            highlightLink={this.props.highlightLink}
+                            showUrlPreview={this.props.showUrlPreview}
+                            tileShape={this.props.tileShape}
+                            onHeightChanged={this.props.onHeightChanged}
+                        />
+                    </div>,
+                    <a
+                        className="mx_EventTile_senderDetailsLink"
+                        key="mx_EventTile_senderDetailsLink"
+                        href={permalink}
+                        onClick={this.onPermalinkClicked}
+                    >
+                        <div className="mx_EventTile_senderDetails">
+                            { sender }
+                            { timestamp }
                         </div>
-                        <a
-                            className="mx_EventTile_senderDetailsLink"
-                            href={permalink}
-                            onClick={this.onPermalinkClicked}
-                        >
-                            <div className="mx_EventTile_senderDetails">
-                                { sender }
-                                { timestamp }
-                            </div>
-                        </a>
-                    </li>
-                );
+                    </a>,
+                ]);
             }
 
             case 'reply':
@@ -1126,27 +1133,30 @@ export default class EventTile extends React.Component<IProps, IState> {
                         this.props.alwaysShowTimestamps || this.state.hover,
                     );
                 }
-                return (
-                    <li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
-                        { ircTimestamp }
-                        { avatar }
-                        { sender }
-                        { ircPadlock }
-                        <div className="mx_EventTile_reply">
-                            { groupTimestamp }
-                            { groupPadlock }
-                            { thread }
-                            <EventTileType ref={this.tile}
-                                mxEvent={this.props.mxEvent}
-                                highlights={this.props.highlights}
-                                highlightLink={this.props.highlightLink}
-                                onHeightChanged={this.props.onHeightChanged}
-                                replacingEventId={this.props.replacingEventId}
-                                showUrlPreview={false}
-                            />
-                        </div>
-                    </li>
-                );
+                return React.createElement(this.props.as || "li", {
+                    "className": classes,
+                    "aria-live": ariaLive,
+                    "aria-atomic": true,
+                    "data-scroll-tokens": scrollToken,
+                }, [
+                    ircTimestamp,
+                    avatar,
+                    sender,
+                    ircPadlock,
+                    <div className="mx_EventTile_reply" key="mx_EventTile_reply">
+                        { groupTimestamp }
+                        { groupPadlock }
+                        { thread }
+                        <EventTileType ref={this.tile}
+                            mxEvent={this.props.mxEvent}
+                            highlights={this.props.highlights}
+                            highlightLink={this.props.highlightLink}
+                            onHeightChanged={this.props.onHeightChanged}
+                            replacingEventId={this.props.replacingEventId}
+                            showUrlPreview={false}
+                        />
+                    </div>,
+                ]);
             }
             default: {
                 const thread = ReplyThread.makeThread(
diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js
index 5835eb9f36..c61424a059 100644
--- a/src/components/views/rooms/ReplyPreview.js
+++ b/src/components/views/rooms/ReplyPreview.js
@@ -95,6 +95,7 @@ export default class ReplyPreview extends React.Component {
                     permalinkCreator={this.props.permalinkCreator}
                     isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
                     enableFlair={SettingsStore.getValue(UIFeature.Flair)}
+                    as="div"
                 />
             </div>
         </div>;

From 84679cf8ec5bfe764cda2743f69a9099fe79c38a Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Fri, 11 Jun 2021 12:19:14 +0100
Subject: [PATCH 119/159] remove legacy polyfills and unused dependencies

---
 package.json                        |  3 ---
 src/ContentMessages.tsx             |  2 --
 src/CountlyAnalytics.ts             |  7 -------
 src/rageshake/submit-rageshake.ts   |  6 ------
 src/utils/MegolmExportEncryption.js | 11 -----------
 yarn.lock                           | 10 ----------
 6 files changed, 39 deletions(-)

diff --git a/package.json b/package.json
index dbb6b29b53..4dc4bda3b2 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,6 @@
   "dependencies": {
     "@babel/runtime": "^7.12.5",
     "await-lock": "^2.1.0",
-    "blueimp-canvas-to-blob": "^3.28.0",
     "browser-encrypt-attachment": "^0.3.0",
     "browser-request": "^0.3.3",
     "cheerio": "^1.0.0-rc.9",
@@ -88,7 +87,6 @@
     "png-chunks-extract": "^1.0.0",
     "prop-types": "^15.7.2",
     "qrcode": "^1.4.4",
-    "qs": "^6.9.6",
     "re-resizable": "^6.9.0",
     "react": "^17.0.2",
     "react-beautiful-dnd": "^4.0.1",
@@ -99,7 +97,6 @@
     "rfc4648": "^1.4.0",
     "sanitize-html": "^2.3.2",
     "tar-js": "^0.3.0",
-    "text-encoding-utf-8": "^1.0.2",
     "url": "^0.11.0",
     "what-input": "^5.2.10",
     "zxcvbn": "^4.4.2"
diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx
index b21829ac63..9042d47243 100644
--- a/src/ContentMessages.tsx
+++ b/src/ContentMessages.tsx
@@ -28,8 +28,6 @@ import encrypt from "browser-encrypt-attachment";
 import extractPngChunks from "png-chunks-extract";
 import Spinner from "./components/views/elements/Spinner";
 
-// Polyfill for Canvas.toBlob API using Canvas.toDataURL
-import "blueimp-canvas-to-blob";
 import { Action } from "./dispatcher/actions";
 import CountlyAnalytics from "./CountlyAnalytics";
 import {
diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts
index 5545ed8483..f494c1eb22 100644
--- a/src/CountlyAnalytics.ts
+++ b/src/CountlyAnalytics.ts
@@ -24,13 +24,6 @@ import {sleep} from "./utils/promise";
 import RoomViewStore from "./stores/RoomViewStore";
 import { Action } from "./dispatcher/actions";
 
-// polyfill textencoder if necessary
-import * as TextEncodingUtf8 from 'text-encoding-utf-8';
-let TextEncoder = window.TextEncoder;
-if (!TextEncoder) {
-    TextEncoder = TextEncodingUtf8.TextEncoder;
-}
-
 const INACTIVITY_TIME = 20; // seconds
 const HEARTBEAT_INTERVAL = 5_000; // ms
 const SESSION_UPDATE_INTERVAL = 60; // seconds
diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts
index b2ad9fe6f6..08d8ccfd13 100644
--- a/src/rageshake/submit-rageshake.ts
+++ b/src/rageshake/submit-rageshake.ts
@@ -25,14 +25,8 @@ import Tar from "tar-js";
 
 import * as rageshake from './rageshake';
 
-// polyfill textencoder if necessary
-import * as TextEncodingUtf8 from 'text-encoding-utf-8';
 import SettingsStore from "../settings/SettingsStore";
 import SdkConfig from "../SdkConfig";
-let TextEncoder = window.TextEncoder;
-if (!TextEncoder) {
-    TextEncoder = TextEncodingUtf8.TextEncoder;
-}
 
 interface IOpts {
     label?: string;
diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js
index 6f5c7104b1..ae6b2999fa 100644
--- a/src/utils/MegolmExportEncryption.js
+++ b/src/utils/MegolmExportEncryption.js
@@ -15,17 +15,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-// polyfill textencoder if necessary
-import * as TextEncodingUtf8 from 'text-encoding-utf-8';
-let TextEncoder = window.TextEncoder;
-if (!TextEncoder) {
-    TextEncoder = TextEncodingUtf8.TextEncoder;
-}
-let TextDecoder = window.TextDecoder;
-if (!TextDecoder) {
-    TextDecoder = TextEncodingUtf8.TextDecoder;
-}
-
 import { _t } from '../languageHandler';
 import SdkConfig from '../SdkConfig';
 
diff --git a/yarn.lock b/yarn.lock
index 2b8667c7a6..f98cb5845e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2181,11 +2181,6 @@ bluebird@^3.5.0:
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
-blueimp-canvas-to-blob@^3.28.0:
-  version "3.28.0"
-  resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.28.0.tgz#c8ab4dc6bb08774a7f273798cdf94b0776adf6c8"
-  integrity sha512-5q+YHzgGsuHQ01iouGgJaPJXod2AzTxJXmVv90PpGrRxU7G7IqgPqWXz+PBmt3520jKKi6irWbNV87DicEa7wg==
-
 boolbase@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -7945,11 +7940,6 @@ test-exclude@^6.0.0:
     glob "^7.1.4"
     minimatch "^3.0.4"
 
-text-encoding-utf-8@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
-  integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
-
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"

From 37484e3fc465ef5a05a5001580facbea37d10bb5 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Fri, 11 Jun 2021 12:33:02 +0100
Subject: [PATCH 120/159] Add TextEncoder polyfill for test run

---
 test/setupTests.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/test/setupTests.js b/test/setupTests.js
index 6d37d48987..9da412a7e8 100644
--- a/test/setupTests.js
+++ b/test/setupTests.js
@@ -1,6 +1,12 @@
 import * as languageHandler from "../src/languageHandler";
+import { TextEncoder, TextDecoder } from 'util';
 
 languageHandler.setLanguage('en');
 languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]);
 
 require('jest-fetch-mock').enableMocks();
+
+// polyfilling TextEncoder as it is not available on JSDOM
+// view https://github.com/facebook/jest/issues/9983
+global.TextEncoder = TextEncoder;
+global.TextDecoder = TextDecoder;

From e260e8671fab08da34f8c9abc30ec39e1aa486d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Fri, 11 Jun 2021 15:14:16 +0200
Subject: [PATCH 121/159] Inline display name into return
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/SenderProfile.tsx | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx
index 872dc132b9..805f842fbc 100644
--- a/src/components/views/messages/SenderProfile.tsx
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -118,12 +118,6 @@ export default class SenderProfile extends React.Component<IProps, IState> {
             return null; // emote message must include the name so don't duplicate it
         }
 
-        const displayNameElement = (
-            <span className={`mx_SenderProfile_displayName ${colorClass}`}>
-                { displayName }
-            </span>
-        );
-
         let mxidElement;
         if (disambiguate) {
             mxidElement = (
@@ -147,7 +141,9 @@ export default class SenderProfile extends React.Component<IProps, IState> {
 
         return (
             <div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
-                { displayNameElement }
+                <span className={`mx_SenderProfile_displayName ${colorClass}`}>
+                    { displayName }
+                </span>
                 { mxidElement }
                 { flair }
             </div>

From 6187863bd18ae3c4048dd4dcb5b377a97c4ac42b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= <meskobalazs@gmail.com>
Date: Tue, 8 Jun 2021 14:32:54 +0000
Subject: [PATCH 122/159] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2979 of 2979 strings)

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

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index e1ccfb288d..90c1aca5e2 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -31,7 +31,7 @@
     "Default Device": "Alapértelmezett eszköz",
     "Microphone": "Mikrofon",
     "Camera": "Kamera",
-    "Advanced": "Haladó",
+    "Advanced": "Speciális",
     "Always show message timestamps": "Üzenet időbélyeg folyamatos megjelenítése",
     "Authentication": "Azonosítás",
     "Failed to change password. Is your password correct?": "Nem sikerült megváltoztatni a jelszót. Helyesen írtad be a jelszavadat?",
@@ -66,7 +66,7 @@
     "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s megváltoztatta a hozzáférési szintjét erre: %(powerLevelDiffText)s.",
     "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s megváltoztatta a szoba nevét erre: %(roomName)s.",
     "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s törölte a szoba nevét.",
-    "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s megváltoztatta a témát erre \"%(topic)s\".",
+    "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s megváltoztatta a témát erre: „%(topic)s”.",
     "Changes your display nickname": "Megváltoztatja a becenevedet",
     "Click here to fix": "A javításhoz kattints ide",
     "Click to mute audio": "Hang némításához kattints ide",
@@ -116,7 +116,7 @@
     "Failed to set display name": "Megjelenítési nevet nem sikerült beállítani",
     "Failed to unban": "Kizárás visszavonása sikertelen",
     "Failed to upload profile picture!": "Profil kép feltöltése sikertelen!",
-    "Failed to verify email address: make sure you clicked the link in the email": "E-mail cím ellenőrzése sikertelen: ellenőrizze, hogy az e-mailben lévő hivatkozásra kattintott-e",
+    "Failed to verify email address: make sure you clicked the link in the email": "Az e-mail-cím ellenőrzése sikertelen: ellenőrizze, hogy az e-mailben lévő hivatkozásra kattintott-e",
     "Failure to create room": "Szoba létrehozása sikertelen",
     "Favourites": "Kedvencek",
     "Fill screen": "Képernyő kitöltése",
@@ -230,7 +230,7 @@
     "Submit": "Elküld",
     "Success": "Sikeres",
     "The phone number entered looks invalid": "A megadott telefonszám érvénytelennek tűnik",
-    "This email address is already in use": "Ez az e-mail cím már használatban van",
+    "This email address is already in use": "Ez az e-mail-cím már használatban van",
     "This email address was not found": "Az e-mail cím nem található",
     "The email address linked to your account must be entered.": "A fiókodhoz kötött e-mail címet add meg.",
     "The remote side failed to pick up": "A hívott fél nem vette fel",
@@ -1545,7 +1545,7 @@
     "Remove %(count)s messages|one": "1 üzenet törlése",
     "Your email address hasn't been verified yet": "Az e-mail-címe még nincs ellenőrizve",
     "Click the link in the email you received to verify and then click continue again.": "Ellenőrzéshez kattints a linkre az e-mailben amit kaptál és itt kattints a folytatásra újra.",
-    "Add Email Address": "E-mail cím hozzáadása",
+    "Add Email Address": "E-mail-cím hozzáadása",
     "Add Phone Number": "Telefonszám hozzáadása",
     "%(creator)s created and configured the room.": "%(creator)s elkészítette és beállította a szobát.",
     "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.": "Először <b>töröld a személyes adatokat</b> az azonosítási szerverről (<idserver />) mielőtt lecsatlakozol. Sajnos az azonosítási szerver (<idserver />) jelenleg elérhetetlen.",
@@ -2109,7 +2109,7 @@
     "Server did not return valid authentication information.": "A szerver semmilyen érvényes azonosítási információt sem küldött vissza.",
     "There was a problem communicating with the server. Please try again.": "A szerverrel való kommunikációval probléma történt. Kérlek próbáld újra.",
     "Sign in with SSO": "Belépés SSO-val",
-    "Welcome to %(appName)s": "Üdvözöl az %(appName)s",
+    "Welcome to %(appName)s": "Üdvözöl a(z) %(appName)s",
     "Liberate your communication": "Kommunikálj szabadon",
     "Send a Direct Message": "Közvetlen üzenet küldése",
     "Explore Public Rooms": "Nyilvános szobák felfedezése",
@@ -2945,7 +2945,7 @@
     "Learn more": "Tudj meg többet",
     "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "A matrix.org a legnagyobb nyilvános Matrix szerver a világon, és sok felhasználónak megfelelő választás.",
     "About homeservers": "A Matrix szerverekről",
-    "Use your preferred Matrix homeserver if you have one, or host your own.": "Add meg az általad választott Matrix szerver címét, ha van ilyen, vagy üzemeltess egy sajátot!",
+    "Use your preferred Matrix homeserver if you have one, or host your own.": "Add meg az általad választott Matrix szerver címét, ha van ilyen, vagy üzemeltess egy sajátot.",
     "Other homeserver": "Másik Matrix szerver",
     "Host account on": "Fiók létrehozása itt:",
     "We call the places where you can host your account ‘homeservers’.": "Matrix szervereknek nevezzük azokat a helyeket, ahol fiókot lehet létrehozni.",
@@ -3107,7 +3107,7 @@
     "Room name": "Szoba neve",
     "Support": "Támogatás",
     "Random": "Véletlen",
-    "Welcome to <name/>": "Üdvözlöm itt: <name/>",
+    "Welcome to <name/>": "Üdvözöl a(z) <name/>",
     "Your private space <name/>": "Privát tere: <name/>",
     "Your public space <name/>": "Nyilvános tere: <name/>",
     "You have been invited to <name/>": "Meghívták ide: <name/>",

From ab5d0e5a761cae7e039d17674212097fc218aa30 Mon Sep 17 00:00:00 2001
From: Percy <perrsig@gmail.com>
Date: Thu, 10 Jun 2021 14:25:43 +0000
Subject: [PATCH 123/159] Translated using Weblate (Chinese (Simplified))

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

diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index af02a40587..3267060d64 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -3274,5 +3274,11 @@
     "Send stickers to your active room as you": "发送贴纸到你所活跃的聊天室",
     "See when people join, leave, or are invited to your active room": "查看人们何时加入、离开或被邀请到你所活跃的聊天室",
     "See when people join, leave, or are invited to this room": "查看人们何时加入、离开或被邀请到这个房间",
-    "Kick, ban, or invite people to this room, and make you leave": "移除、封禁或邀请用户到此聊天室,并让你离开"
+    "Kick, ban, or invite people to this room, and make you leave": "移除、封禁或邀请用户到此聊天室,并让你离开",
+    "Currently joining %(count)s rooms|one": "目前正在加入 %(count)s 个聊天室",
+    "Currently joining %(count)s rooms|other": "目前正在加入 %(count)s 个聊天室",
+    "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "尝试不同的单词或检查拼写错误。某些结果可能不可见,因为它们属于私有的,你需要一个邀请才能加入。",
+    "No results for \"%(query)s\"": "「%(query)s」没有结果",
+    "The user you called is busy.": "你所拨打的用户正在忙碌中。",
+    "User Busy": "用户正在忙"
 }

From 96f6ca81a0e6244415221c0bda3f74d982458363 Mon Sep 17 00:00:00 2001
From: Ihor Hordiichuk <igor_ck@outlook.com>
Date: Tue, 8 Jun 2021 03:06:40 +0000
Subject: [PATCH 124/159] Translated using Weblate (Ukrainian)

Currently translated at 50.1% (1493 of 2979 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 | 36 +++++++++++++++++++++++++++++++-----
 1 file changed, 31 insertions(+), 5 deletions(-)

diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index 64ba84b678..92da704837 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -306,8 +306,8 @@
     "Missing user_id in request": "У запиті пропущено user_id",
     "Usage": "Використання",
     "Searches DuckDuckGo for results": "Здійснює пошук через DuckDuckGo",
-    "/ddg is not a command": "/ddg — це не команда",
-    "To use it, just wait for autocomplete results to load and tab through them.": "Щоб цим скористатися, просто почекайте на підказки доповнення й перемикайтеся між ними клавішею TAB.",
+    "/ddg is not a command": "/ddg не є командою",
+    "To use it, just wait for autocomplete results to load and tab through them.": "Щоб цим скористатися, просто почекайте на підказки автодоповнення й перемикайтеся між ними клавішею TAB.",
     "Changes your display nickname": "Змінює ваш нік",
     "Invites user with given id to current room": "Запрошує користувача з вказаним ідентифікатором до кімнати",
     "Leave room": "Залишити кімнату",
@@ -457,7 +457,7 @@
     "Actions": "Дії",
     "Other": "Інше",
     "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Додає ¯\\_(ツ)_/¯ на початку текстового повідомлення",
-    "Sends a message as plain text, without interpreting it as markdown": "Надсилає повідомлення як чистий текст, не використовуючи markdown",
+    "Sends a message as plain text, without interpreting it as markdown": "Надсилає повідомлення у вигляді звичайного тексту, не інтерпретуючи його як розмітку",
     "Upgrades a room to a new version": "Покращує кімнату до нової версії",
     "You do not have the required permissions to use this command.": "Вам бракує дозволу на використання цієї команди.",
     "<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> Ми опублікуємо посилання на нову кімнату у старій версії кімнати, а учасники мають власноруч клацнути це посилання, щоб приєднатися до нової кімнати.",
@@ -624,7 +624,7 @@
     "Riot is now Element!": "Riot тепер - Element!",
     "Learn More": "Дізнатися більше",
     "Command error": "Помилка команди",
-    "Sends a message as html, without interpreting it as markdown": "Надсилає повідомлення як HTML, не інтерпритуючи Markdown",
+    "Sends a message as html, without interpreting it as markdown": "Надсилає повідомлення у вигляді HTML, не інтерпретуючи його як розмітку",
     "Failed to set topic": "Не вдалося встановити тему",
     "Once enabled, encryption cannot be disabled.": "Після увімкнення шифрування не можна буде вимкнути.",
     "Please enter verification code sent via text.": "Будь ласка, введіть звірювальний код, відправлений у текстовому повідомленні.",
@@ -1604,5 +1604,31 @@
     "Open": "Відкрити",
     "<a>In reply to</a> <pill>": "<a>У відповідь на</a> <pill>",
     "The user you called is busy.": "Користувач, якого ви викликаєте, зайнятий.",
-    "User Busy": "Користувач зайнятий"
+    "User Busy": "Користувач зайнятий",
+    "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Додає ┬──┬ ノ( ゜-゜ノ) на початку текстового повідомлення",
+    "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Додає (╯°□°)╯︵ ┻━┻ на початку текстового повідомлення",
+    "We couldn't log you in": "Нам не вдалося виконати вхід",
+    "You're already in a call with this person.": "Ви вже спілкуєтесь із цією особою.",
+    "Already in call": "Вже у виклику",
+    "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "Ви не можете надсилати жодних повідомлень, поки не переглянете та не погодитесь з <consentLink>нашими умовами та положеннями</consentLink>.",
+    "Send message": "Надіслати повідомлення",
+    "Sending your message...": "Надсилання повідомлення...",
+    "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "Ви можете скористатися <code>/help</code> для перегляду доступних команд. Ви мали намір надіслати це як повідомлення?",
+    "Send messages": "Надіслати повідомлення",
+    "sends confetti": "надсилає конфеті",
+    "sends fireworks": "надсилає феєрверк",
+    "sends space invaders": "надсилає тему про космічних загарбників",
+    "Sends the given message with a space themed effect": "Надсилає це повідомлення з космічними ефектами",
+    "unknown person": "невідома особа",
+    "Sends the given message with snowfall": "Надсилає це повідомлення зі снігопадом",
+    "Sends the given message with fireworks": "Надсилає це повідомлення з феєрверком",
+    "Sends the given message with confetti": "Надсилає це повідомлення з конфеті",
+    "Use Ctrl + Enter to send a message": "Натисніть Ctrl + Enter, щоб надіслати повідомлення",
+    "Use Command + Enter to send a message": "Натисніть Command + Enter, щоб надіслати повідомлення",
+    "Use Ctrl + F to search": "Натисніть Ctrl + F, щоб шукати",
+    "Use Command + F to search": "Натисніть Command + F, щоб шукати",
+    "Send text messages as you in this room": "Надіслати текстові повідомлення у цю кімнату від свого імені",
+    "Send messages as you in your active room": "Надіслати повідомлення у свою активну кімнату від свого імені",
+    "Send messages as you in this room": "Надіслати повідомлення у цю кімнату від свого імені",
+    "Sends the given message as a spoiler": "Надсилає вказане повідомлення згорненим"
 }

From 5de7541c51195b7784328f478dc6aaabdbdd1b99 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sim=C3=B3=20Albert=20i=20Beltran?= <sim6@probeta.net>
Date: Tue, 8 Jun 2021 22:28:32 +0000
Subject: [PATCH 125/159] Translated using Weblate (Catalan)

Currently translated at 27.5% (822 of 2979 strings)

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

diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json
index 9a9e0efaa7..9bbc861732 100644
--- a/src/i18n/strings/ca.json
+++ b/src/i18n/strings/ca.json
@@ -373,21 +373,21 @@
     "Manage Integrations": "Gestiona les integracions",
     "%(nameList)s %(transitionList)s": "%(transitionList)s%(nameList)s",
     "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s s'hi han unit",
-    "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s s'ha unit",
+    "%(oneUser)sjoined %(count)s times|one": "%(oneUser)ss'ha unit",
     "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s han sortit",
-    "%(oneUser)sleft %(count)s times|one": "%(oneUser)s ha sortit",
+    "%(oneUser)sleft %(count)s times|one": "%(oneUser)sha sortit",
     "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s s'hi han unit i han sortit %(count)s vegades",
     "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s s'hi han unit i han sortit",
-    "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s ha entrat i ha sortit %(count)s vegades",
-    "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s ha entrat i ha sortit",
+    "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sha entrat i ha sortit %(count)s vegades",
+    "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sha entrat i ha sortit",
     "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s han sortit i han tornat a entrar %(count)s vegades",
     "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s han sortit i han tornat a entrar",
-    "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s ha sortit i ha tornat a entrar %(count)s vegades",
-    "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s ha sortit i ha tornat a entrar",
+    "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sha sortit i ha tornat a entrar %(count)s vegades",
+    "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sha sortit i ha tornat a entrar",
     "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s han rebutjat les seves invitacions %(count)s vegades",
     "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s han rebutjat les seves invitacions",
-    "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s ha rebutjat la seva invitació %(count)s vegades",
-    "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s ha rebutjat la seva invitació",
+    "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)sha rebutjat la seva invitació %(count)s vegades",
+    "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)sha rebutjat la seva invitació",
     "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "S'han retirat les invitacions de %(severalUsers)s %(count)s vegades",
     "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "S'han retirat les invitacions de %(severalUsers)s",
     "%(oneUser)shad their invitation withdrawn %(count)s times|other": "S'ha retirat la invitació de %(oneUser)s %(count)s vegades",
@@ -410,12 +410,12 @@
     "was kicked %(count)s times|one": "l'han fet fora",
     "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s han canviat el seu nom %(count)s vegades",
     "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s han canviat el seu nom",
-    "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s ha canviat el seu nom %(count)s vegades",
+    "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)sha canviat el seu nom %(count)s vegades",
     "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s ha canviat el seu nom",
     "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s han canviat el seu avatar %(count)s vegades",
     "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s han canviat el seu avatar",
-    "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s han canviat el seu avatar %(count)s vegades",
-    "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s ha canviat el seu avatar",
+    "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)shan canviat el seu avatar %(count)s vegades",
+    "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)sha canviat el seu avatar",
     "%(items)s and %(count)s others|other": "%(items)s i %(count)s altres",
     "%(items)s and %(count)s others|one": "%(items)s i un altre",
     "%(items)s and %(lastItem)s": "%(items)s i %(lastItem)s",
@@ -437,9 +437,9 @@
     "Showing flair for these communities:": "Mostra els talents d'aquestes comunitats:",
     "Display your community flair in rooms configured to show it.": "Mostra els talents de la vostra comunitat dins les sales configurades per a mostrar-los.",
     "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s han entrat %(count)s vegades",
-    "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s ha entrat %(count)s vegades",
+    "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sha entrat %(count)s vegades",
     "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s han sortit %(count)s vegades",
-    "%(oneUser)sleft %(count)s times|other": "%(oneUser)s ha sortit %(count)s vegades",
+    "%(oneUser)sleft %(count)s times|other": "%(oneUser)sha sortit %(count)s vegades",
     "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Les ID de les comunitats només poden contendre caràcters a-z, 0-9, o '=_-./'",
     "Community IDs cannot be empty.": "Les ID de les comunitats no poden estar buides.",
     "Something went wrong whilst creating your community": "S'ha produït un error mentre es creava la comunitat",
@@ -722,7 +722,7 @@
     "Failed to invite users to the room:": "No s'han pogut convidar els usuaris a la sala:",
     "Missing roomId.": "Falta l'ID de sala.",
     "Searches DuckDuckGo for results": "Cerca al DuckDuckGo els resultats",
-    "Changes your display nickname": "Canvia el teu àlies de visualització",
+    "Changes your display nickname": "Canvia l'àlies a mostrar",
     "Invites user with given id to current room": "Convida a la sala actual l'usuari amb l'ID indicat",
     "Kicks user with given id": "Expulsa l'usuari amb l'ID indicat",
     "Bans user with given id": "Bandeja l'usuari amb l'ID indicat",
@@ -854,7 +854,7 @@
     "Changes your avatar in all rooms": "Canvia el teu avatar en totes les sales",
     "Changes your avatar in this current room only": "Canvia el teu avatar només en aquesta sala actual",
     "Changes the avatar of the current room": "Canvia l'avatar de la sala actual",
-    "Changes your display nickname in the current room only": "Canvia el teu àlies de visualització només en la sala actual",
+    "Changes your display nickname in the current room only": "Canvia el teu àlies a mostrar només en la sala actual",
     "Double check that your server supports the room version chosen and try again.": "Comprova que el teu servidor és compatible amb la versió de sala que has triat i torna-ho a intentar.",
     "You do not have the required permissions to use this command.": "No disposes dels permisos necessaris per utilitzar aquesta ordre.",
     "Sends a message as html, without interpreting it as markdown": "Envia un missatge com a html sense interpretar-lo com a markdown",
@@ -951,5 +951,7 @@
     "Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic.",
     "Unable to access webcam / microphone": "No s'ha pogut accedir a la càmera web / micròfon",
     "Unable to access microphone": "No s'ha pogut accedir al micròfon",
-    "Explore rooms": "Explora sales"
+    "Explore rooms": "Explora sales",
+    "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sno ha fet canvis",
+    "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sno ha fet canvis %(count)s cops"
 }

From 373404b1fdc8bccfc988d81ea7e80ad73b5b6a02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adri=C3=A0?= <adria@fsfe.org>
Date: Tue, 8 Jun 2021 22:19:25 +0000
Subject: [PATCH 126/159] Translated using Weblate (Catalan)

Currently translated at 27.5% (822 of 2979 strings)

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

diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json
index 9bbc861732..3fdf1e0b4f 100644
--- a/src/i18n/strings/ca.json
+++ b/src/i18n/strings/ca.json
@@ -82,7 +82,7 @@
     "Add rooms to the community": "Afegeix sales a la comunitat",
     "Add to community": "Afegeix a la comunitat",
     "Failed to invite the following users to %(groupId)s:": "No s'han pogut convidar a %(groupId)s els següents usuaris:",
-    "Failed to invite users to community": "No s'han pogut convidar els usuaris a la comunitat",
+    "Failed to invite users to community": "No s'ha pogut convidar els usuaris a la comunitat",
     "Failed to invite users to %(groupId)s": "No s'han pogut convidar els usuaris a %(groupId)s",
     "Failed to add the following rooms to %(groupId)s:": "No s'han pogut afegir a %(groupId)s les següents sales:",
     "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s no té permís per enviar-te notificacions, comprova la configuració del teu navegador",
@@ -371,7 +371,7 @@
     "Communities": "Comunitats",
     "Home": "Inici",
     "Manage Integrations": "Gestiona les integracions",
-    "%(nameList)s %(transitionList)s": "%(transitionList)s%(nameList)s",
+    "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
     "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s s'hi han unit",
     "%(oneUser)sjoined %(count)s times|one": "%(oneUser)ss'ha unit",
     "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s han sortit",

From e8cc12c94b445363748883c6072b87cdc8fdef82 Mon Sep 17 00:00:00 2001
From: Evilham <github@evilham.com>
Date: Tue, 8 Jun 2021 22:14:11 +0000
Subject: [PATCH 127/159] Translated using Weblate (Catalan)

Currently translated at 27.5% (822 of 2979 strings)

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

diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json
index 3fdf1e0b4f..945b5a10cc 100644
--- a/src/i18n/strings/ca.json
+++ b/src/i18n/strings/ca.json
@@ -78,13 +78,13 @@
     "Invite new community members": "Convida nous membres a la comunitat",
     "Invite to Community": "Convida a la comunitat",
     "Which rooms would you like to add to this community?": "Quines sales vols afegir a aquesta comunitat?",
-    "Show these rooms to non-members on the community page and room list?": "Vols mostrar aquestes sales a la pàgina de la comunitat i a la llista de sales per als que no hi son membres?",
+    "Show these rooms to non-members on the community page and room list?": "Voleu mostrar aquestes sales a la pàgina de la comunitat i al llistat de sales, als qui no en siguin membres?",
     "Add rooms to the community": "Afegeix sales a la comunitat",
     "Add to community": "Afegeix a la comunitat",
     "Failed to invite the following users to %(groupId)s:": "No s'han pogut convidar a %(groupId)s els següents usuaris:",
     "Failed to invite users to community": "No s'ha pogut convidar els usuaris a la comunitat",
     "Failed to invite users to %(groupId)s": "No s'han pogut convidar els usuaris a %(groupId)s",
-    "Failed to add the following rooms to %(groupId)s:": "No s'han pogut afegir a %(groupId)s les següents sales:",
+    "Failed to add the following rooms to %(groupId)s:": "No s'ha pogut afegir a %(groupId)s les següents sales:",
     "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s no té permís per enviar-te notificacions, comprova la configuració del teu navegador",
     "%(brand)s was not given permission to send notifications - please try again": "%(brand)s no ha rebut cap permís per enviar notificacions, torna-ho a provar",
     "Unable to enable Notifications": "No s'han pogut activar les notificacions",

From f79323471970ac27ec7ab69b2ffd9ab2e679ba9e Mon Sep 17 00:00:00 2001
From: BruceyZG <zganecro@gmail.com>
Date: Wed, 9 Jun 2021 15:52:45 +0000
Subject: [PATCH 128/159] Translated using Weblate (Croatian)

Currently translated at 6.9% (207 of 2979 strings)

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

diff --git a/src/i18n/strings/hr.json b/src/i18n/strings/hr.json
index 2511771578..8070757426 100644
--- a/src/i18n/strings/hr.json
+++ b/src/i18n/strings/hr.json
@@ -5,5 +5,205 @@
     "The platform you're on": "Platforma na kojoj se nalazite",
     "The version of %(brand)s": "Verzija %(brand)s",
     "Your language of choice": "Izabrani jezik",
-    "Dismiss": "Odbaci"
+    "Dismiss": "Odbaci",
+    "France": "Francuska",
+    "Finland": "Finska",
+    "Fiji": "Fiji",
+    "Faroe Islands": "Farski otoci",
+    "Falkland Islands": "Falklandski otoci",
+    "Ethiopia": "Etiopija",
+    "Estonia": "Estonija",
+    "Eritrea": "Eritreja",
+    "Equatorial Guinea": "Ekvatorska Gvineja",
+    "El Salvador": "El Salvador",
+    "Egypt": "Egipat",
+    "Ecuador": "Ekvador",
+    "Dominican Republic": "Dominikanska Republika",
+    "Dominica": "Dominika",
+    "Djibouti": "Džibuti",
+    "Denmark": "Danska",
+    "Côte d’Ivoire": "Obala Bjelokosti",
+    "Czech Republic": "Češka",
+    "Cyprus": "Cipar",
+    "Curaçao": "Curaçao",
+    "Cuba": "Kuba",
+    "Croatia": "Hrvatska",
+    "Costa Rica": "Kostarika",
+    "Cook Islands": "Cookovo Otočje",
+    "Congo - Kinshasa": "Kongo - Kinshasa",
+    "Congo - Brazzaville": "Republika Kongo",
+    "Comoros": "Komori",
+    "Colombia": "Kolumbija",
+    "Cocos (Keeling) Islands": "Kokosovi (Keeling) otoci",
+    "Christmas Island": "Uskršnji otoci",
+    "China": "Kina",
+    "Chile": "Čile",
+    "Chad": "Čad",
+    "Central African Republic": "Srednjoafrička Republika",
+    "Cayman Islands": "Kajmanski otoci",
+    "Caribbean Netherlands": "Karipska Nizozemska",
+    "Cape Verde": "Zelenortski Otoci",
+    "Canada": "Kanada",
+    "Cameroon": "Kamerun",
+    "Cambodia": "Kambodža",
+    "Burundi": "Burundi",
+    "Burkina Faso": "Burkina Faso",
+    "Bulgaria": "Bugarska",
+    "Brunei": "Brunej",
+    "British Virgin Islands": "Britanski djevičanski otoci",
+    "British Indian Ocean Territory": "Britanski teritorij Indijskog oceana",
+    "Brazil": "Brazil",
+    "Bouvet Island": "Otok Bouvet",
+    "Botswana": "Bocvana",
+    "Bosnia": "Bosna i Hercegovina",
+    "Bolivia": "Bolivija",
+    "Bhutan": "Butan",
+    "Bermuda": "Bermuda",
+    "Benin": "Benin",
+    "Belize": "Belize",
+    "Belgium": "Belgija",
+    "Belarus": "Bjelorusija",
+    "Barbados": "Barbados",
+    "Bangladesh": "Bangladeš",
+    "Bahrain": "Bahrein",
+    "Bahamas": "Bahami",
+    "Azerbaijan": "Azerbejdžan",
+    "Austria": "Austrija",
+    "Australia": "Australija",
+    "Aruba": "Aruba",
+    "Armenia": "Armenija",
+    "Argentina": "Argentina",
+    "Antigua & Barbuda": "Antigva i Barbuda",
+    "Antarctica": "Antartika",
+    "Anguilla": "Angvila",
+    "Angola": "Angola",
+    "Andorra": "Andora",
+    "American Samoa": "Američka Samoa",
+    "Algeria": "Alžir",
+    "Albania": "Albanija",
+    "Åland Islands": "Alandski otoci",
+    "Afghanistan": "Afganistan",
+    "United States": "Sjedinjene Države",
+    "United Kingdom": "Ujedinjeno Kraljevstvo",
+    "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Čini se da Vaša email adresa nije povezana s Matrix ID-om na ovom kućnom poslužitelju.",
+    "This email address was not found": "Ova email adresa nije pronađena",
+    "Unable to enable Notifications": "Omogućavanje notifikacija nije uspjelo",
+    "%(brand)s was not given permission to send notifications - please try again": "%(brand)s nema dopuštenje slati Vam notifikacije - molimo pokušajte ponovo",
+    "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s nema dopuštenje slati Vam notifikacije - molimo provjerite postavke pretraživača",
+    "%(name)s is requesting verification": "%(name)s traži potvrdu",
+    "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.": "Vaš je kućni poslužitelj odbio vaš pokušaj prijave. Razlog je možda da je jednostavno sve predugo trajalo. Molimo pokušajte ponovno. Ako se ovo nastavi, obratite se administratoru kućnog poslužitelja.",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Vaš kućni poslužitelj nije bio dostupan i nije vas mogao prijaviti. Pokušajte ponovo. Ako se ovo nastavi, obratite se administratoru kućnog poslužitelja.",
+    "Try again": "Pokušaj ponovo",
+    "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.": "Tražili smo od preglednika da zapamti koji kućni poslužitelj koristite za prijavu, ali ga je Vaš preglednik nažalost zaboravio. Idite na stranicu za prijavu i pokušajte ponovo.",
+    "We couldn't log you in": "Nismo Vas mogli ulogirati",
+    "Trust": "Vjeruj",
+    "Only continue if you trust the owner of the server.": "Nastavite samo ako vjerujete vlasniku poslužitelja.",
+    "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.": "Ova radnja zahtijeva pristup zadanom poslužitelju identiteta <server /> radi provjere adrese e-pošte ili telefonskog broja, no poslužitelj nema nikakve uvjete usluge.",
+    "Identity server has no terms of service": "Poslužitelj identiteta nema uvjete usluge",
+    "Unnamed Room": "Neimenovana soba",
+    "Failed to add the following rooms to %(groupId)s:": "Neuspješno dodavanje sljedećih soba u %(groupId)s:",
+    "Failed to invite users to %(groupId)s": "Neuspješno dodavanje korisnika u %(groupId)s",
+    "Failed to invite users to community": "Dodavanje korisnika u zajednicu nije uspjelo",
+    "Failed to invite the following users to %(groupId)s:": "Neuspješno dodavanje sljedećih korisnika u %(groupId)s:",
+    "Add to community": "Dodaj u zajednicu",
+    "Room name or address": "Ime ili adresa sobe",
+    "Add rooms to the community": "Dodaj sobe zajednici",
+    "Show these rooms to non-members on the community page and room list?": "Prikaži ove sobe osobama koje nisu članovi na stranici zajednice i popisu soba?",
+    "Which rooms would you like to add to this community?": "Koju sobu biste željeli dodati u ovu zajednicu?",
+    "Invite to Community": "Pozovi u zajednicu",
+    "Name or Matrix ID": "Ime ili Matrix ID",
+    "Invite new community members": "Pozovite nove članove zajednice",
+    "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Upozorenje: svaka osoba koju dodate u zajednicu bit će javno vidljiva svima koji znaju ID zajednice",
+    "Who would you like to add to this community?": "Koga biste željeli dodati u ovu zajednicu?",
+    "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s, %(time)s",
+    "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s",
+    "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s. %(monthName)s, %(time)s",
+    "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
+    "AM": "Prijepodne",
+    "PM": "Poslijepodne",
+    "Dec": "Pro",
+    "Nov": "Stu",
+    "Oct": "Lis",
+    "Sep": "Ruj",
+    "Aug": "Kol",
+    "Jul": "Srp",
+    "Jun": "Lip",
+    "May": "Svi",
+    "Apr": "Tra",
+    "Mar": "Ožu",
+    "Feb": "Velj",
+    "Jan": "Sij",
+    "Sat": "Sub",
+    "Fri": "Pet",
+    "Thu": "Čet",
+    "Wed": "Sri",
+    "Tue": "Uto",
+    "Mon": "Pon",
+    "Sun": "Ned",
+    "Failure to create room": "Stvaranje sobe neuspješno",
+    "The server does not support the room version specified.": "Poslužitelj ne podržava navedenu verziju sobe.",
+    "Server may be unavailable, overloaded, or you hit a bug.": "Poslužitelj je možda nedostupan, preopterećen, ili ste pronašli grešku u aplikaciji.",
+    "Upload Failed": "Prijenos neuspješan",
+    "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Datoteka '%(fileName)s' premašuje maksimalnu veličinu ovog kućnog poslužitelja za prijenose",
+    "The file '%(fileName)s' failed to upload.": "Prijenos datoteke '%(fileName)s' nije uspio.",
+    "Continue": "Nastavi",
+    "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Trenutno nije moguće odgovoriti datotekom. Želite li prenijeti ovu datoteku bez odgovora?",
+    "Replying With Files": "Odgovaranje datotekama",
+    "This will end the conference for everyone. Continue?": "To će prekinuti konferenciju za sve. Nastaviti?",
+    "End conference": "Završi konferenciju",
+    "You do not have permission to start a conference call in this room": "Nemate dopuštenje uspostaviti konferencijski poziv u ovoj sobi",
+    "Permission Required": "Potrebno dopuštenje",
+    "A call is currently being placed!": "Poziv se upravo uspostavlja!",
+    "Call in Progress": "Poziv u tijeku",
+    "You cannot place a call with yourself.": "Ne možete uspostaviti poziv sami sa sobom.",
+    "You're already in a call with this person.": "Već ste u pozivu sa tom osobom.",
+    "Already in call": "Već u pozivu",
+    "You've reached the maximum number of simultaneous calls.": "Dosegli ste maksimalan broj istodobnih poziva.",
+    "Too Many Calls": "Previše poziva",
+    "You cannot place VoIP calls in this browser.": "Ne možete uspostaviti VoIP pozive u ovom pretraživaču.",
+    "VoIP is unsupported": "VoIP nije podržan",
+    "Unable to capture screen": "Nije moguće snimanje zaslona",
+    "No other application is using the webcam": "Da ni jedna druga aplikacija već ne koristi web kameru",
+    "Permission is granted to use the webcam": "Jeli dopušteno korištenje web kamere",
+    "A microphone and webcam are plugged in and set up correctly": "Jesu li mikrofon i web kamera priključeni i pravilno postavljeni",
+    "Unable to access webcam / microphone": "Nije moguće pristupiti web kameri / mikrofonu",
+    "Call failed because webcam or microphone could not be accessed. Check that:": "Poziv nije uspio jer nije bilo moguće pristupiti web kameri ili mikrofonu. Provjerite:",
+    "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Poziv nije uspio jer nije bilo moguće pristupiti mikrofonu. Provjerite je li mikrofon priključen i ispravno postavljen.",
+    "Unable to access microphone": "Nije moguće pristupiti mikrofonu",
+    "OK": "OK",
+    "Try using turn.matrix.org": "Pokušajte koristiti turn.matrix.org",
+    "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.": "Alternativno, možete pokušati koristiti javni poslužitelj na <code>turn.matrix.org</code>, no to bi moglo biti manje pouzdano i Vaša IP adresa će biti podijeljena s tim poslužiteljem. Time također možete upravljati u Postavkama.",
+    "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Zamolite administratora Vašeg kućnog poslužitelja (<code>%(homeserverDomain)s</code>) da konfigurira TURN poslužitelj kako bi pozivi mogli pouzdano funkcionirati.",
+    "Call failed due to misconfigured server": "Poziv neuspješan radi pogrešno konfiguriranog poslužitelja",
+    "The call was answered on another device.": "Na poziv je odgovoreno sa drugog uređaja.",
+    "Answered Elsewhere": "Odgovoreno je drugdje",
+    "The call could not be established": "Poziv se nije mogao uspostaviti",
+    "The remote side failed to pick up": "Sugovornik nije odgovorio na poziv",
+    "The user you called is busy.": "Pozvani korisnik je zauzet.",
+    "User Busy": "Korisnik zauzet",
+    "The other party declined the call.": "Sugovornik je odbio poziv.",
+    "Call Declined": "Poziv odbijen",
+    "Call Failed": "Poziv neuspješan",
+    "Unable to load! Check your network connectivity and try again.": "Učitavanje nije moguće! Provjerite mrežnu povezanost i pokušajte ponovo.",
+    "Error": "Geška",
+    "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Gdje ova stranica uključuje identificirajuće podatke, poput ID-a sobe, korisnika ili grupe, ti se podaci uklanjaju prije slanja na poslužitelj.",
+    "The information being sent to us to help make %(brand)s better includes:": "Podaci koji nam se šalju radi poboljšanja %(brand)s uključuju:",
+    "Analytics": "Analitika",
+    "Your device resolution": "Razlučivost vašeg uređaja",
+    "Your user agent": "Vaš korisnički agent",
+    "e.g. <CurrentPageURL>": "npr. <CurrentPageURL>",
+    "Every page you use in the app": "Svaka stranica koju upotrebljavate u aplikaciji",
+    "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Bez obzira upotrebljavate li %(brand)s na uređaju na kojem je dodir primarni mehanizam unosa",
+    "Confirm adding this phone number by using Single Sign On to prove your identity.": "Potvrdite dodavanje ovog telefonskog broja koristeći jedinstvenu prijavu (SSO) da biste dokazali Vaš identitet.",
+    "Confirm adding this email address by using Single Sign On to prove your identity.": "Potvrdite dodavanje ove email adrese koristeći jedinstvenu prijavu (SSO) da biste dokazali Vaš identitet.",
+    "Single Sign On": "Jedinstvena prijava (SSO)",
+    "Use Single Sign On to continue": "Koristite jedinstvenu prijavu (SSO) za nastavak",
+    "Whether or not you're logged in (we don't record your username)": "Bez obzira jeste li ulogirani ili ne (ne snimamo vaše korisničko ime)",
+    "Add Phone Number": "Dodaj telefonski broj",
+    "Click the button below to confirm adding this phone number.": "Kliknite gumb ispod da biste potvrdili dodavanje ovog telefonskog broja.",
+    "Confirm adding phone number": "Potvrdite dodavanje telefonskog broja",
+    "Add Email Address": "Dodaj email adresu",
+    "Confirm": "Potvrdi",
+    "Click the button below to confirm adding this email address.": "Kliknite gumb ispod da biste potvrdili dodavanje ove email adrese.",
+    "Confirm adding email": "Potvrdite dodavanje email adrese"
 }

From 7000176572e048ca542cf9cb56d2a26ddc76ca70 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Mon, 14 Jun 2021 14:53:22 +0100
Subject: [PATCH 129/159] Add workflow steps to track measurements

---
 .github/workflows/develop.yml  | 21 +++++++++++++++++----
 test/end-to-end-tests/start.js |  8 ++++++--
 2 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index 3f82e61280..749999cfc3 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -20,8 +20,21 @@ jobs:
                     test/end-to-end-tests/logs/**/*
                     test/end-to-end-tests/synapse/installations/consent/homeserver.log
                 retention-days: 14
-            - name: Archive performance benchmark
-              uses: actions/upload-artifact@v2
+            - name: Download previous benchmark data
+              uses: actions/cache@v1
               with:
-                name: performance-entries.json
-                path: test/end-to-end-tests/performance-entries.json
+                path: ./cache
+                key: ${{ runner.os }}-benchmark
+            - name: Temporary step before having a fully release GitHub action
+              run: npm install && npm run build
+              working-directory: /home/runner/work/_actions/matrix-org/github-action-benchmark/9f891b47906b73678ba486f7a53e4807e24fff19
+            - name: Store benchmark result
+              uses: matrix-org/github-action-benchmark@9f891b47906b73678ba486f7a53e4807e24fff19
+              with:
+                tool: 'jsperformanceentry'
+                output-file-path: test/end-to-end-tests/performance-entries.json
+                external-data-json-path: ./cache/benchmark-data-template.json
+                fail-on-alert: false
+            - name: Push benchmark result
+              if: ${{ github.ref == 'refs/heads/develop' }}
+              run: git push 'https://matrixbot:${{ secrets.DEPLOY_GH_PAGES }}@github.com/matrix-org/matrix-react-sdk.git' gh-pages:gh-pages
diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js
index f29b485c84..c1588e848e 100644
--- a/test/end-to-end-tests/start.js
+++ b/test/end-to-end-tests/start.js
@@ -79,7 +79,7 @@ async function runTests() {
         await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000));
     }
 
-    const performanceEntries = {};
+    let performanceEntries;
 
     await Promise.all(sessions.map(async (session) => {
         // Collecting all performance monitoring data before closing the session
@@ -95,7 +95,11 @@ async function runTests() {
             }, true);
             return measurements;
         });
-        performanceEntries[session.username] = JSON.parse(measurements);
+
+        /**
+         * TODO: temporary only use one user session data
+         */
+        performanceEntries = JSON.parse(measurements);
         return session.close();
     }));
     fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries));

From 6e3ece2dc615a005bf5e06bbd5735a5f98e55bc6 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Mon, 14 Jun 2021 15:29:18 +0100
Subject: [PATCH 130/159] use proper released github action

---
 .github/workflows/develop.yml | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index 749999cfc3..fe744a5aa8 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -25,11 +25,8 @@ jobs:
               with:
                 path: ./cache
                 key: ${{ runner.os }}-benchmark
-            - name: Temporary step before having a fully release GitHub action
-              run: npm install && npm run build
-              working-directory: /home/runner/work/_actions/matrix-org/github-action-benchmark/9f891b47906b73678ba486f7a53e4807e24fff19
             - name: Store benchmark result
-              uses: matrix-org/github-action-benchmark@9f891b47906b73678ba486f7a53e4807e24fff19
+              uses: matrix-org/github-action-benchmark@jsperfentry
               with:
                 tool: 'jsperformanceentry'
                 output-file-path: test/end-to-end-tests/performance-entries.json

From 2eafe132b8b0650b058faa4be3bbdc4e5167ff38 Mon Sep 17 00:00:00 2001
From: Travis Ralston <travisr@matrix.org>
Date: Mon, 14 Jun 2021 13:31:58 -0600
Subject: [PATCH 131/159] Move various createRoom types to the js-sdk

---
 src/components/structures/SpaceRoomView.tsx   |  3 +-
 .../views/dialogs/CreateRoomDialog.tsx        | 21 ++++----
 .../views/spaces/SpaceCreateMenu.tsx          |  6 ++-
 src/createRoom.ts                             | 52 ++-----------------
 src/mjolnir/Mjolnir.ts                        |  3 +-
 5 files changed, 23 insertions(+), 62 deletions(-)

diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 276f4ae6ca..3ccf2e5424 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -28,7 +28,7 @@ import RoomTopic from "../views/elements/RoomTopic";
 import InlineSpinner from "../views/elements/InlineSpinner";
 import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
 import {useRoomMembers} from "../../hooks/useRoomMembers";
-import createRoom, {IOpts, Preset} from "../../createRoom";
+import createRoom, {IOpts} from "../../createRoom";
 import Field from "../views/elements/Field";
 import {useEventEmitter} from "../../hooks/useEventEmitter";
 import withValidation from "../views/elements/Validation";
@@ -65,6 +65,7 @@ import dis from "../../dispatcher/dispatcher";
 import Modal from "../../Modal";
 import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
 import SdkConfig from "../../SdkConfig";
+import { Preset } from "matrix-js-sdk/src/@types/partials";
 
 interface IProps {
     space: Room;
diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx
index cce6b6c34c..544f593708 100644
--- a/src/components/views/dialogs/CreateRoomDialog.tsx
+++ b/src/components/views/dialogs/CreateRoomDialog.tsx
@@ -15,22 +15,23 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {ChangeEvent, createRef, KeyboardEvent, SyntheticEvent} from "react";
-import {Room} from "matrix-js-sdk/src/models/room";
+import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
 
 import SdkConfig from '../../../SdkConfig';
-import withValidation, {IFieldState} from '../elements/Validation';
-import {_t} from '../../../languageHandler';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
-import {Key} from "../../../Keyboard";
-import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom";
-import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import withValidation, { IFieldState } from '../elements/Validation';
+import { _t } from '../../../languageHandler';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import { Key } from "../../../Keyboard";
+import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
+import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
 import Field from "../elements/Field";
 import RoomAliasField from "../elements/RoomAliasField";
 import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import DialogButtons from "../elements/DialogButtons";
 import BaseDialog from "../dialogs/BaseDialog";
+import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
 
 interface IProps {
     defaultPublic?: boolean;
@@ -72,7 +73,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
             canChangeEncryption: true,
         };
 
-        MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
+        MatrixClientPeg.get().doesServerForceEncryptionForPreset(Preset.PrivateChat)
             .then(isForced => this.setState({ canChangeEncryption: !isForced }));
     }
 
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 0ebf511018..6a935ab276 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -22,7 +22,7 @@ import FocusLock from "react-focus-lock";
 import {_t} from "../../../languageHandler";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
-import createRoom, {IStateEvent, Preset} from "../../../createRoom";
+import createRoom from "../../../createRoom";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {SpaceAvatar} from "./SpaceBasicSettings";
 import AccessibleButton from "../elements/AccessibleButton";
@@ -33,6 +33,8 @@ import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
 import Field from "../elements/Field";
 import withValidation from "../elements/Validation";
 import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
+import { Preset } from "matrix-js-sdk/src/@types/partials";
+import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
 
 const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
     return (
@@ -81,7 +83,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
             return;
         }
 
-        const initialState: IStateEvent[] = [
+        const initialState: ICreateRoomStateEvent[] = [
             {
                 type: EventType.RoomHistoryVisibility,
                 content: {
diff --git a/src/createRoom.ts b/src/createRoom.ts
index c6507b1380..2641492588 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -35,53 +35,15 @@ import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
 import SpaceStore from "./stores/SpaceStore";
 import { makeSpaceParentEvent } from "./utils/space";
 import { Action } from "./dispatcher/actions"
+import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
+import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
 
 // we define a number of interfaces which take their names from the js-sdk
 /* eslint-disable camelcase */
 
-// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
-export enum Visibility {
-    Public = "public",
-    Private = "private",
-}
-
-export enum Preset {
-    PrivateChat = "private_chat",
-    TrustedPrivateChat = "trusted_private_chat",
-    PublicChat = "public_chat",
-}
-
-interface Invite3PID {
-    id_server: string;
-    id_access_token?: string; // this gets injected by the js-sdk
-    medium: string;
-    address: string;
-}
-
-export interface IStateEvent {
-    type: string;
-    state_key?: string; // defaults to an empty string
-    content: object;
-}
-
-interface ICreateOpts {
-    visibility?: Visibility;
-    room_alias_name?: string;
-    name?: string;
-    topic?: string;
-    invite?: string[];
-    invite_3pid?: Invite3PID[];
-    room_version?: string;
-    creation_content?: object;
-    initial_state?: IStateEvent[];
-    preset?: Preset;
-    is_direct?: boolean;
-    power_level_content_override?: object;
-}
-
 export interface IOpts {
     dmUserId?: string;
-    createOpts?: ICreateOpts;
+    createOpts?: ICreateRoomOpts;
     spinner?: boolean;
     guestAccess?: boolean;
     encryption?: boolean;
@@ -91,12 +53,6 @@ export interface IOpts {
     parentSpace?: Room;
 }
 
-export interface IInvite3PID {
-    id_server: string,
-    medium: 'email',
-    address: string,
-}
-
 /**
  * Create a new room, and switch to it.
  *
@@ -136,7 +92,7 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
     const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat;
 
     // set some defaults for the creation
-    const createOpts = opts.createOpts || {};
+    const createOpts: ICreateRoomOpts = opts.createOpts || {};
     createOpts.preset = createOpts.preset || defaultPreset;
     createOpts.visibility = createOpts.visibility || Visibility.Private;
     if (opts.dmUserId && createOpts.invite === undefined) {
diff --git a/src/mjolnir/Mjolnir.ts b/src/mjolnir/Mjolnir.ts
index 891438bbb9..f1fe816642 100644
--- a/src/mjolnir/Mjolnir.ts
+++ b/src/mjolnir/Mjolnir.ts
@@ -20,6 +20,7 @@ import SettingsStore from "../settings/SettingsStore";
 import {_t} from "../languageHandler";
 import dis from "../dispatcher/dispatcher";
 import {SettingLevel} from "../settings/SettingLevel";
+import { Preset } from "matrix-js-sdk/src/@types/partials";
 
 // TODO: Move this and related files to the js-sdk or something once finalized.
 
@@ -86,7 +87,7 @@ export class Mjolnir {
             const resp = await MatrixClientPeg.get().createRoom({
                 name: _t("My Ban List"),
                 topic: _t("This is your list of users/servers you have blocked - don't leave the room!"),
-                preset: "private_chat",
+                preset: Preset.PrivateChat,
             });
             personalRoomId = resp['room_id'];
             await SettingsStore.setValue(

From 951fbf4a1a06f52d40b2eefc21937a3383e983ea Mon Sep 17 00:00:00 2001
From: Travis Ralston <travisr@matrix.org>
Date: Mon, 14 Jun 2021 13:38:43 -0600
Subject: [PATCH 132/159] fix import

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

diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index b006205f11..d90214110f 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -31,7 +31,6 @@ import Modal from "../../../Modal";
 import {humanizeTime} from "../../../utils/humanize";
 import createRoom, {
     canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
-    IInvite3PID,
 } from "../../../createRoom";
 import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
 import {Key} from "../../../Keyboard";
@@ -50,6 +49,7 @@ import {getAddressType} from "../../../UserAddress";
 import BaseAvatar from '../avatars/BaseAvatar';
 import AccessibleButton from '../elements/AccessibleButton';
 import { compare } from '../../../utils/strings';
+import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
 
 // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
 /* eslint-disable camelcase */

From f8eea2ae23ccf9bc58af983d236249d1ed878831 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 14 Jun 2021 22:18:02 +0100
Subject: [PATCH 133/159] Fix word wrap in space descriptions

---
 res/css/structures/_SpaceRoomView.scss | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 12762cbc9d..48b565be7f 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -328,7 +328,8 @@ $SpaceRoomViewInnerWidth: 428px;
             font-size: $font-15px;
             margin-top: 12px;
             margin-bottom: 16px;
-            white-space: pre;
+            white-space: pre-wrap;
+            word-wrap: break-word;
         }
 
         > hr {

From 430ec8cecd002cbc5996ec313bb0f7322a6eee14 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 14 Jun 2021 22:18:18 +0100
Subject: [PATCH 134/159] Clear selected rooms after operation in manage space
 rooms

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

diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index 8d59fe6c68..acbde0b097 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -520,6 +520,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
                             setError("Failed to update some suggestions. Try again later");
                         }
                         setSaving(false);
+                        setSelected(new Map());
                     }}
                     kind="primary_outline"
                     disabled={disabled}

From d08495bde68f206ba39d3941950a3a34f279abbd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com>
Date: Tue, 15 Jun 2021 06:30:22 +0200
Subject: [PATCH 135/159] Fix display name overlap
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/_IRCLayout.scss | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss
index cf61ce569d..ae8d22a462 100644
--- a/res/css/views/rooms/_IRCLayout.scss
+++ b/res/css/views/rooms/_IRCLayout.scss
@@ -178,7 +178,7 @@ $irc-line-height: $font-18px;
         overflow: hidden;
         display: flex;
 
-        > .mx_SenderProfile_name {
+        > .mx_SenderProfile_displayName {
             overflow: hidden;
             text-overflow: ellipsis;
             min-width: var(--name-width);
@@ -207,7 +207,7 @@ $irc-line-height: $font-18px;
             background: transparent;
 
             > span {
-                > .mx_SenderProfile_name {
+                > .mx_SenderProfile_displayName {
                     min-width: inherit;
                 }
             }

From 646416c8ec6b984e3971ed9d1ecd215b7d40eeb4 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 15 Jun 2021 08:59:51 +0100
Subject: [PATCH 136/159] try deployment to gh-pages

---
 .github/workflows/develop.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index fe744a5aa8..fe4061d268 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -33,5 +33,5 @@ jobs:
                 external-data-json-path: ./cache/benchmark-data-template.json
                 fail-on-alert: false
             - name: Push benchmark result
-              if: ${{ github.ref == 'refs/heads/develop' }}
-              run: git push 'https://matrixbot:${{ secrets.DEPLOY_GH_PAGES }}@github.com/matrix-org/matrix-react-sdk.git' gh-pages:gh-pages
+            #   if: ${{ github.ref == 'refs/heads/develop' }}
+              run: git push 'https://RiotRobot:${{ secrets.DEPLOY_GH_PAGES }}@github.com/matrix-org/matrix-react-sdk.git' gh-pages:gh-pages

From 6f6a128587e26d1e7e0c3826651db25db5f0e9a0 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 15 Jun 2021 09:27:44 +0100
Subject: [PATCH 137/159] try auto push

---
 .github/workflows/develop.yml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index fe4061d268..83dc884af4 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -30,8 +30,8 @@ jobs:
               with:
                 tool: 'jsperformanceentry'
                 output-file-path: test/end-to-end-tests/performance-entries.json
-                external-data-json-path: ./cache/benchmark-data-template.json
                 fail-on-alert: false
-            - name: Push benchmark result
-            #   if: ${{ github.ref == 'refs/heads/develop' }}
-              run: git push 'https://RiotRobot:${{ secrets.DEPLOY_GH_PAGES }}@github.com/matrix-org/matrix-react-sdk.git' gh-pages:gh-pages
+                # Personal access token to deploy GitHub Pages branch
+                github-token: ${{ secrets.DEPLOY_GH_PAGES }}
+                # Push and deploy GitHub pages branch automatically
+                auto-push: true

From 55a9915c55e5579e9a90c4c164c38df591a290a8 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 15 Jun 2021 10:01:05 +0100
Subject: [PATCH 138/159] Upgrade github-action-benchmark

---
 .github/workflows/develop.yml | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index 83dc884af4..0e382aae8b 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -26,12 +26,13 @@ jobs:
                 path: ./cache
                 key: ${{ runner.os }}-benchmark
             - name: Store benchmark result
-              uses: matrix-org/github-action-benchmark@jsperfentry
+              uses: matrix-org/github-action-benchmark@jsperfentry-1
               with:
                 tool: 'jsperformanceentry'
                 output-file-path: test/end-to-end-tests/performance-entries.json
                 fail-on-alert: false
                 # Personal access token to deploy GitHub Pages branch
-                github-token: ${{ secrets.DEPLOY_GH_PAGES }}
+                github-token: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
                 # Push and deploy GitHub pages branch automatically
                 auto-push: true
+                # auto-push: ${{ github.ref == 'refs/heads/develop' }}

From 2b2e83b9e755f98de9287ed6d19c2bff098f09b9 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 15 Jun 2021 10:10:22 +0100
Subject: [PATCH 139/159] Fix github token

---
 .github/workflows/develop.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index 0e382aae8b..00752a8693 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -32,7 +32,7 @@ jobs:
                 output-file-path: test/end-to-end-tests/performance-entries.json
                 fail-on-alert: false
                 # Personal access token to deploy GitHub Pages branch
-                github-token: ${{ secrets.PERSONAL_GITHUB_TOKEN }}
+                github-token: ${{ secrets.DEPLOY_GH_PAGES }}
                 # Push and deploy GitHub pages branch automatically
                 auto-push: true
                 # auto-push: ${{ github.ref == 'refs/heads/develop' }}

From d5f7f524e13b580dc5ed0384f4b1930f9a65ea60 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 15 Jun 2021 10:24:07 +0100
Subject: [PATCH 140/159] Slow down registration process

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

diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 16da9321e2..c2ec313907 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -1953,6 +1953,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         // Create and start the client
         await Lifecycle.setLoggedIn(credentials);
         await this.postLoginSetup();
+
+        // artifically slowing down the registration
+        await (new Promise((resolve) => {
+            setTimeout(resolve, 1337);
+        }));
+
         PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN);
         PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
     };

From 011a2f2bb6075767b20506874ff7797fddf81175 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 15 Jun 2021 10:59:57 +0100
Subject: [PATCH 141/159] Remove test data for develop pipeline

---
 .github/workflows/develop.yml            | 8 ++++----
 src/components/structures/MatrixChat.tsx | 5 -----
 2 files changed, 4 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index 00752a8693..273dea1062 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -31,8 +31,8 @@ jobs:
                 tool: 'jsperformanceentry'
                 output-file-path: test/end-to-end-tests/performance-entries.json
                 fail-on-alert: false
-                # Personal access token to deploy GitHub Pages branch
+                comment-on-alert: true
+                # Only temporary to monitor where failures occur
+                alert-comment-cc-users: '@gsouquet'
                 github-token: ${{ secrets.DEPLOY_GH_PAGES }}
-                # Push and deploy GitHub pages branch automatically
-                auto-push: true
-                # auto-push: ${{ github.ref == 'refs/heads/develop' }}
+                auto-push: ${{ github.ref == 'refs/heads/develop' }}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index c2ec313907..0af2d3d635 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -1954,11 +1954,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         await Lifecycle.setLoggedIn(credentials);
         await this.postLoginSetup();
 
-        // artifically slowing down the registration
-        await (new Promise((resolve) => {
-            setTimeout(resolve, 1337);
-        }));
-
         PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN);
         PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
     };

From 4a23ebae1e45c572e07cde7eee68d7738b302292 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 15 Jun 2021 12:00:44 +0100
Subject: [PATCH 142/159] upgrade matrix-react-test-utils for react 17 peer
 deps

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

diff --git a/package.json b/package.json
index 4dc4bda3b2..d8c26098ca 100644
--- a/package.json
+++ b/package.json
@@ -157,7 +157,7 @@
     "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",
+    "matrix-react-test-utils": "^0.2.3",
     "matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
     "react-test-renderer": "^17.0.2",
     "rimraf": "^3.0.2",
diff --git a/yarn.lock b/yarn.lock
index f98cb5845e..7c232d2aa1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5733,10 +5733,10 @@ matrix-mock-request@^1.2.3:
     bluebird "^3.5.0"
     expect "^1.20.2"
 
-matrix-react-test-utils@^0.2.2:
-  version "0.2.2"
-  resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
-  integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
+matrix-react-test-utils@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.3.tgz#27653f9d6bbfddd1856e51860fad1503b039d617"
+  integrity sha512-NKZDlMEQzDZDQhBYyKBUtqidRvpkww3n9/GmGICkxtU2D6NetyBIfvm1Lf9o7167KSkPHJUVvDS9dzaS55jUnA==
 
 "matrix-web-i18n@github:matrix-org/matrix-web-i18n":
   version "1.1.2"

From 8dbc5bddc0ddfbd7496af656eb65b0c02b529de7 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 15 Jun 2021 14:47:45 +0100
Subject: [PATCH 143/159] Disable comment-on-alert for PR coming from a fork

---
 .github/workflows/develop.yml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index 273dea1062..b57a5de8c1 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -31,7 +31,9 @@ jobs:
                 tool: 'jsperformanceentry'
                 output-file-path: test/end-to-end-tests/performance-entries.json
                 fail-on-alert: false
-                comment-on-alert: true
+                # Secrets are not passed to fork, the action won't be able to comment
+                # for community PRs
+                comment-on-alert: ${{ github.repository_owner == 'matrix-org' }}
                 # Only temporary to monitor where failures occur
                 alert-comment-cc-users: '@gsouquet'
                 github-token: ${{ secrets.DEPLOY_GH_PAGES }}

From adc4bd14c0f360c1bae445e7b13375c2fdc26051 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Tue, 15 Jun 2021 15:19:47 +0100
Subject: [PATCH 144/159] Disable comment-on-alert

---
 .github/workflows/develop.yml | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml
index b57a5de8c1..4e8cdff139 100644
--- a/.github/workflows/develop.yml
+++ b/.github/workflows/develop.yml
@@ -31,9 +31,7 @@ jobs:
                 tool: 'jsperformanceentry'
                 output-file-path: test/end-to-end-tests/performance-entries.json
                 fail-on-alert: false
-                # Secrets are not passed to fork, the action won't be able to comment
-                # for community PRs
-                comment-on-alert: ${{ github.repository_owner == 'matrix-org' }}
+                comment-on-alert: false
                 # Only temporary to monitor where failures occur
                 alert-comment-cc-users: '@gsouquet'
                 github-token: ${{ secrets.DEPLOY_GH_PAGES }}

From 7bc305f0d4008ceab90d3465e010010e6586cc8d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= <meskobalazs@gmail.com>
Date: Tue, 15 Jun 2021 08:27:36 +0000
Subject: [PATCH 145/159] Translated using Weblate (Hungarian)

Currently translated at 100.0% (2979 of 2979 strings)

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

diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index 90c1aca5e2..b290f20076 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -26,7 +26,7 @@
     "Admin Tools": "Admin. Eszközök",
     "No Microphones detected": "Nem található mikrofon",
     "No Webcams detected": "Nem található webkamera",
-    "No media permissions": "Nincs media jogosultság",
+    "No media permissions": "Nincs média jogosultság",
     "You may need to manually permit %(brand)s to access your microphone/webcam": "Lehet hogy kézileg kell engedélyeznie a %(brand)snak, hogy hozzáférjen a mikrofonjához és webkamerájához",
     "Default Device": "Alapértelmezett eszköz",
     "Microphone": "Mikrofon",
@@ -1030,7 +1030,7 @@
     "Room list": "Szoba lista",
     "Timeline": "Idővonal",
     "Autocomplete delay (ms)": "Automatikus kiegészítés késleltetése (ms)",
-    "Roles & Permissions": "Szerepek & Jogosultságok",
+    "Roles & Permissions": "Szerepek és jogosultságok",
     "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "A üzenetek olvashatóságának változtatása csak az új üzenetekre lesz érvényes. A régi üzenetek láthatósága nem fog változni.",
     "Security & Privacy": "Biztonság és adatvédelem",
     "Encryption": "Titkosítás",
@@ -1373,7 +1373,7 @@
     "Message edits": "Üzenet szerkesztések",
     "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:": "A szoba frissítéséhez be kell zárnod ezt a szobát és egy újat kell nyitnod e helyett. A szoba tagjainak a legjobb felhasználói élmény nyújtásához az alábbit fogjuk tenni:",
     "Loading room preview": "Szoba előnézetének a betöltése",
-    "Show all": "Mindet mutat",
+    "Show all": "Mind megjelenítése",
     "%(senderName)s made no change.": "%(senderName)s nem változtatott semmit.",
     "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s %(count)s alkalommal nem változtattak semmit",
     "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s nem változtattak semmit",
@@ -1531,7 +1531,7 @@
     "%(count)s unread messages including mentions.|other": "%(count)s olvasatlan üzenet megemlítéssel.",
     "%(count)s unread messages.|other": "%(count)s olvasatlan üzenet.",
     "Unread mentions.": "Olvasatlan megemlítés.",
-    "Show image": "Képek megmutatása",
+    "Show image": "Kép megjelenítése",
     "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Ahhoz hogy a hibát megvizsgálhassuk kérlek <newIssueLink>készíts egy új hibajegyet</newIssueLink> a GitHubon.",
     "To continue you need to accept the terms of this service.": "A folytatáshoz el kell fogadnod a felhasználási feltételeket.",
     "Document": "Dokumentum",
@@ -1622,7 +1622,7 @@
     "Subscribing to a ban list will cause you to join it!": "A feliratkozás egy tiltó listára azzal jár, hogy csatlakozol hozzá!",
     "If this isn't what you want, please use a different tool to ignore users.": "Ha nem ez az amit szeretnél, kérlek használj más eszközt a felhasználók figyelmen kívül hagyásához.",
     "Subscribe": "Feliratkozás",
-    "You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. <a>Mindenképpen megmutat.</a>",
+    "You have ignored this user, so their message is hidden. <a>Show anyways.</a>": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. <a>Megjelenítés mindenképpen.</a>",
     "Custom (%(level)s)": "Egyéni (%(level)s)",
     "Trusted": "Megbízható",
     "Not trusted": "Megbízhatatlan",
@@ -1733,7 +1733,7 @@
     "Country Dropdown": "Ország lenyíló menü",
     "The message you are trying to send is too large.": "Túl nagy képet próbálsz elküldeni.",
     "Help": "Súgó",
-    "Show more": "Mutass többet",
+    "Show more": "Több megjelenítése",
     "Recent Conversations": "Legújabb Beszélgetések",
     "Direct Messages": "Közvetlen Beszélgetések",
     "Go": "Menj",
@@ -1795,7 +1795,7 @@
     "This bridge was provisioned by <user />.": "Ezt a hidat az alábbi felhasználó készítette: <user />.",
     "Workspace: %(networkName)s": "Munkahely: %(networkName)s",
     "Channel: %(channelName)s": "Csatorna: %(channelName)s",
-    "Show less": "Kevesebbet mutat",
+    "Show less": "Kevesebb megjelenítése",
     "Securely cache encrypted messages locally for them to appear in search results, using ": "A titkosított üzenetek kereséséhez azokat biztonságos módon helyileg kell tárolnod, felhasználva: ",
     " to store messages from ": " üzenetek tárolásához ",
     "rooms.": "szobából.",
@@ -2237,7 +2237,7 @@
     "Sort by": "Rendezés",
     "Unread rooms": "Olvasatlan szobák",
     "Always show first": "Mindig az elsőt mutatja",
-    "Show": "Mutat",
+    "Show": "Megjelenítés",
     "Message preview": "Üzenet előnézet",
     "List options": "Lista beállításai",
     "Show %(count)s more|other": "Még %(count)s megjelenítése",

From 5509fe455e151774ae7616db5778f3e3e87a3ec7 Mon Sep 17 00:00:00 2001
From: lvre <7uu3qrbvm@relay.firefox.com>
Date: Sun, 13 Jun 2021 00:00:01 +0000
Subject: [PATCH 146/159] Translated using Weblate (Portuguese (Brazil))

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

diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index 8497ae7164..e19febd6ef 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -163,7 +163,7 @@
     "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s enviou um convite para %(targetDisplayName)s entrar na sala.",
     "%(senderName)s set a profile picture.": "%(senderName)s definiu uma foto de perfil.",
     "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s definiu o nome e sobrenome para %(displayName)s.",
-    "This email address is already in use": "Este endereço de e-mail já está em uso",
+    "This email address is already in use": "Este endereço de email já está em uso",
     "This email address was not found": "Este endereço de e-mail não foi encontrado",
     "The remote side failed to pick up": "A pessoa não atendeu a chamada",
     "This room is not recognised.": "Esta sala não é reconhecida.",
@@ -284,7 +284,7 @@
     "ex. @bob:example.com": "p.ex: @joao:exemplo.com",
     "Add User": "Adicionar usuária(o)",
     "Custom Server Options": "Opções para Servidor Personalizado",
-    "Dismiss": "Descartar",
+    "Dismiss": "Dispensar",
     "Please check your email to continue registration.": "Por favor, confirme o seu e-mail para continuar a inscrição.",
     "Token incorrect": "Token incorreto",
     "Please enter the code it contains:": "Por favor, entre com o código que está na mensagem:",
@@ -1176,7 +1176,7 @@
     "Sign In or Create Account": "Faça login ou crie uma conta",
     "Use your account or create a new one to continue.": "Use sua conta ou crie uma nova para continuar.",
     "Create Account": "Criar Conta",
-    "Sign In": "Entrar",
+    "Sign In": "Fazer signin",
     "Custom (%(level)s)": "Personalizado (%(level)s)",
     "Messages": "Mensagens",
     "Actions": "Ações",

From 6adbc9012731d8a3eb18d5710373ffbb4165f720 Mon Sep 17 00:00:00 2001
From: Victor Grousset <victor@tuxayo.net>
Date: Sat, 12 Jun 2021 22:47:06 +0000
Subject: [PATCH 147/159] Translated using Weblate (Esperanto)

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

diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json
index 1aa5ba8a52..41bb44ed83 100644
--- a/src/i18n/strings/eo.json
+++ b/src/i18n/strings/eo.json
@@ -1591,10 +1591,10 @@
     "Room Autocomplete": "Memkompletigo de ĉambroj",
     "User Autocomplete": "Memkompletigo de uzantoj",
     "Custom (%(level)s)": "Propra (%(level)s)",
-    "%(senderName)s placed a voice call.": "%(senderName)s metis voĉvokon.",
-    "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s metis voĉvokon. (mankas subteno en ĉi tiu foliumilo)",
-    "%(senderName)s placed a video call.": "%(senderName)s metis vidvokon.",
-    "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s metis vidvokon. (mankas subteno en ĉi tiu foliumilo)",
+    "%(senderName)s placed a voice call.": "%(senderName)s ekigis voĉvokon.",
+    "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s ekigis voĉvokon. (mankas subteno en ĉi tiu foliumilo)",
+    "%(senderName)s placed a video call.": "%(senderName)s ekigis vidvokon.",
+    "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s ekigis vidvokon. (mankas subteno en ĉi tiu foliumilo)",
     "Try out new ways to ignore people (experimental)": "Elprovi novajn manierojn malatenti personojn (eksperimente)",
     "Match system theme": "Similiĝi la sisteman haŭton",
     "My Ban List": "Mia listo de forbaroj",

From 24bdcf48a9aa54f09a708b318c6be459c66fd4c2 Mon Sep 17 00:00:00 2001
From: m4sk1n <me@m4sk.in>
Date: Sun, 13 Jun 2021 13:51:26 +0000
Subject: [PATCH 148/159] Translated using Weblate (Polish)

Currently translated at 73.2% (2181 of 2979 strings)

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

diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json
index 83c6c25833..51ba733cfa 100644
--- a/src/i18n/strings/pl.json
+++ b/src/i18n/strings/pl.json
@@ -2271,5 +2271,86 @@
     "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.": "Poprosiliśmy przeglądarkę o zapamiętanie, z którego serwera głównego korzystasz, aby umożliwić Ci logowanie, ale niestety Twoja przeglądarka o tym zapomniała. Przejdź do strony logowania i spróbuj ponownie.",
     "We couldn't log you in": "Nie mogliśmy Cię zalogować",
     "You're already in a call with this person.": "Prowadzisz już rozmowę z tą osobą.",
-    "Already in call": "Już dzwoni"
+    "Already in call": "Już dzwoni",
+    "Never send encrypted messages to unverified sessions in this room from this session": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych sesji z tej sesji w tym pokoju",
+    "Use the <a>Desktop app</a> to see all encrypted files": "Użyj <a>aplikacji desktopowej</a>, aby zobaczyć wszystkie szyfrowane pliki",
+    "You’re all caught up": "Jesteś na bieżąco",
+    "Verification requested": "Zażądano weryfikacji",
+    "You have no visible notifications.": "Nie masz widocznych powiadomień.",
+    "Connecting": "Łączenie",
+    "Your Security Key has been <b>copied to your clipboard</b>, paste it to:": "Twój klucz bezpieczeństwa został <b>skopiowany do schowka</b>, wklej go:",
+    "Your Security Key": "Twój klucz bezpieczeństwa",
+    "Create key backup": "Utwórz kopię zapasową klucza",
+    "Generate a Security Key": "Wygeneruj klucz bezpieczeństwa",
+    "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Zabezpiecz się przed utratą dostępu do szyfrowanych wiadomości i danych, tworząc kopię zapasową kluczy szyfrowania na naszym serwerze.",
+    "Safeguard against losing access to encrypted messages & data": "Zabezpiecz się przed utratą dostępu do szyfrowanych wiadomości i danych",
+    "Access Token": "Token dostępu",
+    "Your access token gives full access to your account. Do not share it with anyone.": "Twój token dostępu daje pełen dostęp do Twojego konta. Nie dziel się nim z nikim.",
+    "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Możesz opuścić betę w każdej chwili z ustawień lub klikając plakietę Beta, taką jak powyższa.",
+    "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta dostępna w przeglądarce, na komputerach i na Androidzie. Niektóre funkcje mogą nie być dostępne na Twoim serwerze.",
+    "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta dostępna w przeglądarce, na komputerach i na Androidzie. Dziękujemy za wypróbowanie bety.",
+    "Avatar": "Awatar",
+    "Leave the beta": "Opuść betę",
+    "Beta": "Beta",
+    "Tap for more info": "Naciśnij aby dowiedzieć się więcej",
+    "Move right": "Przenieś w prawo",
+    "Move left": "Przenieś w lewo",
+    "Join the beta": "Dołącz do bety",
+    "To join %(spaceName)s, turn on the <a>Spaces beta</a>": "Aby dołączyć do %(spaceName)s, włącz <a>betę Przestrzeni</a>",
+    "No results found": "Nie znaleziono wyników",
+    "Create room": "Utwórz pokój",
+    "Removing...": "Usuwanie…",
+    "Your server does not support showing space hierarchies.": "Twój serwer nie obsługuje wyświetlania hierarchii przestrzeni.",
+    "You can select all or individual messages to retry or delete": "Możesz zaznaczyć wszystkie lub wybrane wiadomości aby spróbować ponownie lub usunąć je",
+    "Sending": "Wysyłanie",
+    "Delete all": "Usuń wszystkie",
+    "Some of your messages have not been sent": "Niektóre z Twoich wiadomości nie zostały wysłane",
+    "Filter rooms and people": "Filtruj pokoje i ludzi",
+    "No results for \"%(query)s\"": "Brak wyników dla „%(query)s”",
+    "Explore rooms in %(communityName)s": "Eksploruj pokoje w %(communityName)s",
+    "Retry all": "Spróbuj ponownie wszystkie",
+    "Suggested": "Polecany",
+    "This room is suggested as a good one to join": "Ten pokój jest polecany jako dobry do dołączenia",
+    "%(count)s rooms|one": "%(count)s pokój",
+    "%(count)s rooms|other": "%(count)s pokoi",
+    "%(count)s members|one": "%(count)s członek",
+    "%(count)s members|other": "%(count)s członkowie",
+    "You don't have permission": "Nie masz uprawnień",
+    "Failed to remove some rooms. Try again later": "Nie udało się usunąć niektórych pokoi. Spróbuj ponownie później",
+    "Select a room below first": "Najpierw wybierz poniższy pokój",
+    "%(count)s rooms and 1 space|one": "%(count)s pokój i jedna przestrzeń",
+    "%(count)s rooms and 1 space|other": "%(count)s pokoi i jedna przestrzeń",
+    "To view %(spaceName)s, turn on the <a>Spaces beta</a>": "Aby wyświetlić %(spaceName)s, włącz <a>betę Przestrzeni</a>",
+    "Spaces are a beta feature.": "Przestrzenie są funkcją beta.",
+    "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s pokój i %(numSpaces)s przestrzeni",
+    "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s pokoi i %(numSpaces)s przestrzeni",
+    "Filter all spaces": "Filtruj wszystkie przestrzenie",
+    "Communities are changing to Spaces": "Społeczności zmieniają się na Przestrzenie",
+    "Spaces is a beta feature": "Przestrzenie są funkcją beta",
+    "You can add existing spaces to a space.": "Możesz dodać istniejące przestrzenie do przestrzeni.",
+    "Filter your rooms and spaces": "Filtruj pokoje i przestrzenie",
+    "%(count)s results in all spaces|one": "%(count)s wynik we wszystkich przestrzeniach",
+    "%(count)s results in all spaces|other": "%(count)s wyników we wszystkich przestrzeniach",
+    "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Przestrzenie to nowy sposób na grupowanie pokoi i ludzi. Aby dołączyć do istniejącej przestrzeni, potrzebujesz zaproszenia.",
+    "Your feedback will help make spaces better. The more detail you can go into, the better.": "Twoje opinie pomogą uczynić Przestrzenie lepszymi. Im bardziej szczegółowo je opiszesz, tym lepiej.",
+    "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s odświeży się z włączonymi Przestrzeniami. Społeczności i niestandardowe tagi zostaną ukryte.",
+    "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Jeżeli opuścisz, %(brand)s odświeży się z wyłączonymi Przestrzeniami. Społeczności i niestandardowe tagi będą z powrotem widoczne.",
+    "Spaces are a new way to group rooms and people.": "Przestrzenie to nowy sposób grupowania pokoi i ludzi.",
+    "Spaces": "Przestrzenie",
+    "Feeling experimental?": "Chcesz eksperymentować?",
+    "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. <a>Learn more</a>.": "Chcesz eksperymentować? Laboratoria to najlepszy sposób na uzyskanie nowości wcześniej, przetestowanie nowych funkcji i pomoc w kształtowaniu ich zanim będą ogólnodostępne. <a>Dowiedz się więcej</a>.",
+    "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Będziemy przechowywać zaszyfrowaną kopię Twoich kluczy na naszym serwerze. Zabezpiecz swoją kopię zapasową frazą bezpieczeństwa.",
+    "Secure Backup": "Bezpieczna kopia zapasowa",
+    "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Pozwól na wykorzystanie peer-to-peer w rozmowach 1:1 (jeżeli włączono, druga strona może zobaczyć Twój adres IP)",
+    "Jump to the bottom of the timeline when you send a message": "Przejdź na dół osi czasu po wysłaniu wiadomości",
+    "Use Ctrl + F to search": "Używaj Ctrl + F do wyszukiwania",
+    "Show line numbers in code blocks": "Pokazuj numery wierszy w blokach kodu",
+    "Expand code blocks by default": "Domyślnie rozwijaj bloki kodu",
+    "Show stickers button": "Pokaż przycisk naklejek",
+    "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Zarządcy integracji otrzymują dane konfiguracji, mogą modyfikować widżety, wysyłać zaproszenia do pokoi i ustawiać poziom uprawnień w Twoim imieniu.",
+    "Converts the DM to a room": "Zmienia wiadomości bezpośrednie w pokój",
+    "Converts the room to a DM": "Zmienia pokój w wiadomość bezpośrednią",
+    "Sends the given message as a spoiler": "Wysyła podaną wiadomość jako spoiler",
+    "User Busy": "Użytkownik zajęty",
+    "The user you called is busy.": "Użytkownik do którego zadzwoniłeś(-aś) jest zajęty."
 }

From 6d56c9eab4d7adadc266acbc4252960a8bb74804 Mon Sep 17 00:00:00 2001
From: Besnik Bleta <besnik@programeshqip.org>
Date: Mon, 14 Jun 2021 10:29:38 +0000
Subject: [PATCH 149/159] Translated using Weblate (Albanian)

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

diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index bbeffefda3..4996decbaf 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -3368,5 +3368,7 @@
     "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provoni fjalë të ndryshme, ose kontrolloni për gabime shkrimi. Disa përfundime mund të mos jenë të dukshme, ngaqë janë private dhe ju duhet një ftesë për të marrë pjesë në to.",
     "No results for \"%(query)s\"": "S’ka përfundime për \"%(query)s\"",
     "The user you called is busy.": "Përdoruesi që thirrët është i zënë.",
-    "User Busy": "Përdoruesi Është i Zënë"
+    "User Busy": "Përdoruesi Është i Zënë",
+    "Kick, ban, or invite people to your active room, and make you leave": "Përzini, dëboni, ose ftoni persona te dhoma juaj aktive, dhe bëni largimin tuaj",
+    "Kick, ban, or invite people to this room, and make you leave": "Përzini, dëboni, ose ftoni persona në këtë dhomë, dhe bëni largimin tuaj"
 }

From f1cd086ae2e469071af26ee39af46f598f5c566b Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Tue, 15 Jun 2021 16:27:18 +0100
Subject: [PATCH 150/159] Cache virtual/native room mappings when they're
 created

Otherwise we look up the mapping immediately afterwards and the
remote echo of the account data hasn't come back yet, so we get
nothing.

Fixes "You're already in a call with this person" bug with virtual
rooms.
---
 src/VoipUserMapper.ts | 18 +++++++++++++++---
 1 file changed, 15 insertions(+), 3 deletions(-)

diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts
index d576a5434c..dacb4262bd 100644
--- a/src/VoipUserMapper.ts
+++ b/src/VoipUserMapper.ts
@@ -24,7 +24,9 @@ import { Room } from 'matrix-js-sdk/src/models/room';
 // is sip virtual: there could be others in the future.
 
 export default class VoipUserMapper {
-    private virtualRoomIdCache = new Set<string>();
+    // We store mappings of virtual -> native room IDs here until the local echo for the
+    // account data arrives.
+    private virtualToNativeRoomIdCache = new Map<string, string>();
 
     public static sharedInstance(): VoipUserMapper {
         if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper();
@@ -49,10 +51,20 @@ export default class VoipUserMapper {
             native_room: roomId,
         });
 
+        this.virtualToNativeRoomIdCache.set(virtualRoomId, roomId);
+
         return virtualRoomId;
     }
 
     public nativeRoomForVirtualRoom(roomId: string): string {
+        const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId);
+        if (cachedNativeRoomId) {
+            console.log(
+                "Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache",
+            );
+            return cachedNativeRoomId;
+        }
+
         const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
         if (!virtualRoom) return null;
         const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
@@ -67,7 +79,7 @@ export default class VoipUserMapper {
     public isVirtualRoom(room: Room): boolean {
         if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
 
-        if (this.virtualRoomIdCache.has(room.roomId)) return true;
+        if (this.virtualToNativeRoomIdCache.has(room.roomId)) return true;
 
         // also look in the create event for the claimed native room ID, which is the only
         // way we can recognise a virtual room we've created when it first arrives down
@@ -110,7 +122,7 @@ export default class VoipUserMapper {
 
             // also put this room in the virtual room ID cache so isVirtualRoom return the right answer
             // in however long it takes for the echo of setAccountData to come down the sync
-            this.virtualRoomIdCache.add(invitedRoom.roomId);
+            this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId);
         }
     }
 }

From 6ee55bb03c28822bdca14aaaa2730d2bba6ffa9f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 15 Jun 2021 16:08:33 +0000
Subject: [PATCH 151/159] Bump postcss from 7.0.35 to 7.0.36

Bumps [postcss](https://github.com/postcss/postcss) from 7.0.35 to 7.0.36.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/7.0.35...7.0.36)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

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

diff --git a/yarn.lock b/yarn.lock
index 7c232d2aa1..cd4a8b0bd6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6583,9 +6583,9 @@ postcss-value-parser@^4.1.0:
   integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
 
 postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.6:
-  version "7.0.35"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24"
-  integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==
+  version "7.0.36"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb"
+  integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==
   dependencies:
     chalk "^2.4.2"
     source-map "^0.6.1"

From a5d608f2af12199223711d3e1a473c21c1498259 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 16 Jun 2021 10:01:23 +0100
Subject: [PATCH 152/159] Keep composer reply when scrolling away from a
 highlighted event

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

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index fe90d2f873..c0ce6ba4c9 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -701,6 +701,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                 room_id: this.state.room.roomId,
                 event_id: this.state.initialEventId,
                 highlighted: false,
+                replyingToEvent: this.state.replyToEvent,
             });
         }
     }

From 069e2e13cf3cc73327027e16d7c2abe1aca106c8 Mon Sep 17 00:00:00 2001
From: Germain Souquet <germain@souquet.com>
Date: Wed, 16 Jun 2021 10:01:30 +0100
Subject: [PATCH 153/159] Migrate MessageTimestamp to TypeScript

---
 ...ssageTimestamp.js => MessageTimestamp.tsx} | 29 ++++++++++---------
 1 file changed, 16 insertions(+), 13 deletions(-)
 rename src/components/views/messages/{MessageTimestamp.js => MessageTimestamp.tsx} (67%)

diff --git a/src/components/views/messages/MessageTimestamp.js b/src/components/views/messages/MessageTimestamp.tsx
similarity index 67%
rename from src/components/views/messages/MessageTimestamp.js
rename to src/components/views/messages/MessageTimestamp.tsx
index a7f350adcd..8b02f6b38e 100644
--- a/src/components/views/messages/MessageTimestamp.js
+++ b/src/components/views/messages/MessageTimestamp.tsx
@@ -16,20 +16,19 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
-import {formatFullDate, formatTime, formatFullTime} from '../../../DateUtils';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils';
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+    ts: number;
+    showTwelveHour?: boolean;
+    showFullDate?: boolean;
+    showSeconds?: boolean;
+}
 
 @replaceableComponent("views.messages.MessageTimestamp")
-export default class MessageTimestamp extends React.Component {
-    static propTypes = {
-        ts: PropTypes.number.isRequired,
-        showTwelveHour: PropTypes.bool,
-        showFullDate: PropTypes.bool,
-        showSeconds: PropTypes.bool,
-    };
-
-    render() {
+export default class MessageTimestamp extends React.Component<IProps> {
+    public render() {
         const date = new Date(this.props.ts);
         let timestamp;
         if (this.props.showFullDate) {
@@ -41,7 +40,11 @@ export default class MessageTimestamp extends React.Component {
         }
 
         return (
-            <span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)} aria-hidden={true}>
+            <span
+                className="mx_MessageTimestamp"
+                title={formatFullDate(date, this.props.showTwelveHour)}
+                aria-hidden={true}
+            >
                 {timestamp}
             </span>
         );

From deb2e8d679f56e833c2ef6147218fd5d85b005fb Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 16 Jun 2021 12:04:01 +0100
Subject: [PATCH 154/159] Remove unused methods

---
 src/components/structures/RoomView.tsx  |  6 ------
 src/components/views/rooms/AuxPanel.tsx | 10 ----------
 2 files changed, 16 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index fe90d2f873..b6fafbaaf2 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -705,12 +705,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         }
     }
 
-    private onLayoutChange = () => {
-        this.setState({
-            layout: SettingsStore.getValue("layout"),
-        });
-    };
-
     private onRightPanelStoreUpdate = () => {
         this.setState({
             showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 6d2ae39059..0e3c58dfd4 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -96,16 +96,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
         }
     }
 
-    onConferenceNotificationClick = (ev, type) => {
-        dis.dispatch({
-            action: 'place_call',
-            type: type,
-            room_id: this.props.room.roomId,
-        });
-        ev.stopPropagation();
-        ev.preventDefault();
-    };
-
     _rateLimitedUpdate = new RateLimitedFunc(() => {
         if (SettingsStore.getValue("feature_state_counters")) {
             this.setState({counters: this._computeCounters()});

From e3a6ce13cd98aa5be02cbbe20bd7cb50f1ce259c Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 16 Jun 2021 12:04:37 +0100
Subject: [PATCH 155/159] Fix tight-loop update issue caused by a broken
 shouldComponentUpdate

---
 src/components/structures/RoomView.tsx | 13 ++++---------
 1 file changed, 4 insertions(+), 9 deletions(-)

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index b6fafbaaf2..1224bdb5ae 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -80,7 +80,6 @@ import { objectHasDiff } from "../../utils/objects";
 import SpaceRoomView from "./SpaceRoomView";
 import { IOpts } from "../../createRoom";
 import { replaceableComponent } from "../../utils/replaceableComponent";
-import { omit } from 'lodash';
 import UIStore from "../../stores/UIStore";
 
 const DEBUG = false;
@@ -572,16 +571,12 @@ export default class RoomView extends React.Component<IProps, IState> {
     shouldComponentUpdate(nextProps, nextState) {
         const hasPropsDiff = objectHasDiff(this.props, nextProps);
 
-        // React only shallow comparison and we only want to trigger
-        // a component re-render if a room requires an upgrade
-        const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
-
-        const state = omit(this.state, ['upgradeRecommendation']);
-        const newState = omit(nextState, ['upgradeRecommendation'])
+        const { upgradeRecommendation, ...state } = this.state;
+        const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState;
 
         const hasStateDiff =
-            objectHasDiff(state, newState) ||
-            (newUpgradeRecommendation.needsUpgrade === true)
+            newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade ||
+            objectHasDiff(state, newState);
 
         return hasPropsDiff || hasStateDiff;
     }

From d87325ae6a665fbf9a8d7d6e44b99435cc032b99 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 16 Jun 2021 12:06:41 +0100
Subject: [PATCH 156/159] Small cleanup around the room status bar and auxpanel
 to prevent redundant state updates

---
 src/components/structures/RoomStatusBar.js         |  2 +-
 src/components/structures/RoomView.tsx             | 14 +++++---------
 src/components/views/rooms/AppsDrawer.js           | 12 ++++--------
 src/components/views/rooms/AuxPanel.tsx            | 13 ++++++-------
 .../views/rooms/RoomUpgradeWarningBar.js           |  2 +-
 5 files changed, 17 insertions(+), 26 deletions(-)

diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index b2f0c70bd7..7d74229421 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -41,7 +41,7 @@ export function getUnsentMessages(room) {
 }
 
 @replaceableComponent("structures.RoomStatusBar")
-export default class RoomStatusBar extends React.Component {
+export default class RoomStatusBar extends React.PureComponent {
     static propTypes = {
         // the room this statusbar is representing.
         room: PropTypes.object.isRequired,
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 1224bdb5ae..d2f90cfa0e 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1633,7 +1633,7 @@ export default class RoomView extends React.Component<IProps, IState> {
         let auxPanelMaxHeight = UIStore.instance.windowHeight -
                 (54 + // height of RoomHeader
                  36 + // height of the status area
-                 51 + // minimum height of the message compmoser
+                 51 + // minimum height of the message composer
                  120); // amount of desired scrollback
 
         // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
@@ -1644,18 +1644,14 @@ export default class RoomView extends React.Component<IProps, IState> {
     };
 
     private onStatusBarVisible = () => {
-        if (this.unmounted) return;
-        this.setState({
-            statusBarVisible: true,
-        });
+        if (this.unmounted || this.state.statusBarVisible) return;
+        this.setState({ statusBarVisible: true });
     };
 
     private onStatusBarHidden = () => {
         // This is currently not desired as it is annoying if it keeps expanding and collapsing
-        if (this.unmounted) return;
-        this.setState({
-            statusBarVisible: false,
-        });
+        if (this.unmounted || !this.state.statusBarVisible) return;
+        this.setState({ statusBarVisible: false });
     };
 
     /**
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index 693ec8bc80..0b32d5d1bb 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -82,13 +82,6 @@ export default class AppsDrawer extends React.Component {
         this.props.resizeNotifier.off("isResizing", this.onIsResizing);
     }
 
-    // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    // eslint-disable-next-line camelcase
-    UNSAFE_componentWillReceiveProps(newProps) {
-        // Room has changed probably, update apps
-        this._updateApps();
-    }
-
     onIsResizing = (resizing) => {
         // This one is the vertical, ie. change height of apps drawer
         this.setState({ resizingVertical: resizing });
@@ -141,7 +134,10 @@ export default class AppsDrawer extends React.Component {
     _getAppsHash = (apps) => apps.map(app => app.id).join("~");
 
     componentDidUpdate(prevProps, prevState) {
-        if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
+        if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
+            // Room has changed, update apps
+            this._updateApps();
+        } else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
             this._loadResizerPreferences();
         }
     }
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 0e3c58dfd4..f89390ea25 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 dis from "../../../dispatcher/dispatcher";
 import AppsDrawer from './AppsDrawer';
 import classNames from 'classnames';
 import RateLimitedFunc from '../../../ratelimitedfunc';
@@ -75,12 +74,14 @@ export default class AuxPanel extends React.Component<IProps, IState> {
 
     componentDidMount() {
         const cli = MatrixClientPeg.get();
-        cli.on("RoomState.events", this._rateLimitedUpdate);
+        if (SettingsStore.getValue("feature_state_counters")) {
+            cli.on("RoomState.events", this._rateLimitedUpdate);
+        }
     }
 
     componentWillUnmount() {
         const cli = MatrixClientPeg.get();
-        if (cli) {
+        if (cli && SettingsStore.getValue("feature_state_counters")) {
             cli.removeListener("RoomState.events", this._rateLimitedUpdate);
         }
     }
@@ -97,9 +98,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
     }
 
     _rateLimitedUpdate = new RateLimitedFunc(() => {
-        if (SettingsStore.getValue("feature_state_counters")) {
-            this.setState({counters: this._computeCounters()});
-        }
+        this.setState({ counters: this._computeCounters() });
     }, 500);
 
     _computeCounters() {
@@ -215,7 +214,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
         }
 
         return (
-            <AutoHideScrollbar className={classes} style={style} >
+            <AutoHideScrollbar className={classes} style={style}>
                 { stateViews }
                 { appsDrawer }
                 { callView }
diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js
index a2d4f92d35..66e76903eb 100644
--- a/src/components/views/rooms/RoomUpgradeWarningBar.js
+++ b/src/components/views/rooms/RoomUpgradeWarningBar.js
@@ -24,7 +24,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 
 @replaceableComponent("views.rooms.RoomUpgradeWarningBar")
-export default class RoomUpgradeWarningBar extends React.Component {
+export default class RoomUpgradeWarningBar extends React.PureComponent {
     static propTypes = {
         room: PropTypes.object.isRequired,
         recommendation: PropTypes.object.isRequired,

From 626d5758207bac8af6fdeeec83d4231a8374ff28 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 16 Jun 2021 12:07:58 +0100
Subject: [PATCH 157/159] tidy AuxPanel TS

---
 src/components/views/rooms/AuxPanel.tsx | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index f89390ea25..74609cca13 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -15,18 +15,18 @@ limitations under the License.
 */
 
 import React from 'react';
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { Room } from 'matrix-js-sdk/src/models/room'
 import AppsDrawer from './AppsDrawer';
 import classNames from 'classnames';
 import RateLimitedFunc from '../../../ratelimitedfunc';
 import SettingsStore from "../../../settings/SettingsStore";
 import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
-import {UIFeature} from "../../../settings/UIFeature";
+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";
+import { objectHasDiff } from "../../../utils/objects";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
 
 interface IProps {
     // js-sdk room object
@@ -68,21 +68,21 @@ export default class AuxPanel extends React.Component<IProps, IState> {
         super(props);
 
         this.state = {
-            counters: this._computeCounters(),
+            counters: this.computeCounters(),
         };
     }
 
     componentDidMount() {
         const cli = MatrixClientPeg.get();
         if (SettingsStore.getValue("feature_state_counters")) {
-            cli.on("RoomState.events", this._rateLimitedUpdate);
+            cli.on("RoomState.events", this.rateLimitedUpdate);
         }
     }
 
     componentWillUnmount() {
         const cli = MatrixClientPeg.get();
         if (cli && SettingsStore.getValue("feature_state_counters")) {
-            cli.removeListener("RoomState.events", this._rateLimitedUpdate);
+            cli.removeListener("RoomState.events", this.rateLimitedUpdate);
         }
     }
 
@@ -97,11 +97,11 @@ export default class AuxPanel extends React.Component<IProps, IState> {
         }
     }
 
-    _rateLimitedUpdate = new RateLimitedFunc(() => {
-        this.setState({ counters: this._computeCounters() });
+    private rateLimitedUpdate = new RateLimitedFunc(() => {
+        this.setState({ counters: this.computeCounters() });
     }, 500);
 
-    _computeCounters() {
+    private computeCounters() {
         const counters = [];
 
         if (this.props.room && SettingsStore.getValue("feature_state_counters")) {

From ab964339d2b28af6d89315e65aba66cae20f1769 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Wed, 16 Jun 2021 12:11:17 +0100
Subject: [PATCH 158/159] Add another setState skip to prevent redundant state
 updates

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

diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index d2f90cfa0e..2c081314de 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1640,7 +1640,9 @@ export default class RoomView extends React.Component<IProps, IState> {
         // but it's better than the video going missing entirely
         if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
 
-        this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
+        if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) {
+            this.setState({ auxPanelMaxHeight });
+        }
     };
 
     private onStatusBarVisible = () => {

From 8f02ca8ce958f1df9537bd9d15efd270100a8188 Mon Sep 17 00:00:00 2001
From: "J. Ryan Stinnett" <jryans@gmail.com>
Date: Wed, 16 Jun 2021 18:00:06 +0100
Subject: [PATCH 159/159] Stop requesting null next replies from the server

A recent change (47e007e08f9bedaf47cf59a63c9bd04219195d76) introduced a
regression where we failed to check whether a reply thread has a next reply.
This meant that we would end up sending `/context/undefined` requests to the
server for every reply thread on every room view.

Fixes https://github.com/vector-im/element-web/issues/17563
Regressed by https://github.com/matrix-org/matrix-react-sdk/pull/6079
---
 src/components/views/elements/ReplyThread.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js
index 81ed360b17..a9b24a306b 100644
--- a/src/components/views/elements/ReplyThread.js
+++ b/src/components/views/elements/ReplyThread.js
@@ -297,6 +297,7 @@ export default class ReplyThread extends React.Component {
     }
 
     async getEvent(eventId) {
+        if (!eventId) return null;
         const event = this.room.findEventById(eventId);
         if (event) return event;