diff --git a/package.json b/package.json
index 6e10bafaea..b73462d188 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
     "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
     "lint": "yarn lint:types && yarn lint:js && yarn lint:style",
     "lint:js": "eslint --max-warnings 0 src test",
+    "lint:js-fix": "eslint --fix src test",
     "lint:types": "tsc --noEmit --jsx react",
     "lint:style": "stylelint 'res/css/**/*.scss'",
     "test": "jest",
@@ -64,8 +65,8 @@
     "counterpart": "^0.18.6",
     "diff-dom": "^4.2.2",
     "diff-match-patch": "^1.0.5",
-    "emojibase-data": "^5.1.1",
-    "emojibase-regex": "^4.1.1",
+    "emojibase-data": "^6.2.0",
+    "emojibase-regex": "^5.1.3",
     "escape-html": "^1.0.3",
     "file-saver": "^2.0.5",
     "filesize": "6.1.0",
diff --git a/res/css/_components.scss b/res/css/_components.scss
index 1146a100b5..bc7d4fc85f 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -162,6 +162,7 @@
 @import "./views/messages/_CreateEvent.scss";
 @import "./views/messages/_DateSeparator.scss";
 @import "./views/messages/_EventTileBubble.scss";
+@import "./views/messages/_CallEvent.scss";
 @import "./views/messages/_MEmoteBody.scss";
 @import "./views/messages/_MFileBody.scss";
 @import "./views/messages/_MImageBody.scss";
@@ -201,6 +202,7 @@
 @import "./views/rooms/_EditMessageComposer.scss";
 @import "./views/rooms/_EntityTile.scss";
 @import "./views/rooms/_EventTile.scss";
+@import "./views/rooms/_EventBubbleTile.scss";
 @import "./views/rooms/_GroupLayout.scss";
 @import "./views/rooms/_IRCLayout.scss";
 @import "./views/rooms/_JumpToBottomButton.scss";
diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss
index 7b975110e1..1a02c0d5ac 100644
--- a/res/css/structures/_FilePanel.scss
+++ b/res/css/structures/_FilePanel.scss
@@ -118,10 +118,6 @@ limitations under the License.
     padding-left: 0px;
 }
 
-.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line {
-    background-color: $primary-bg-color;
-}
-
 .mx_FilePanel_empty::before {
     mask-image: url('$(res)/img/element-icons/room/files.svg');
 }
diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss
index cbddd97e18..65e4493f19 100644
--- a/res/css/views/avatars/_BaseAvatar.scss
+++ b/res/css/views/avatars/_BaseAvatar.scss
@@ -27,6 +27,7 @@ limitations under the License.
     // https://bugzilla.mozilla.org/show_bug.cgi?id=255139
     display: inline-block;
     user-select: none;
+    line-height: 1;
 }
 
 .mx_BaseAvatar_initial {
diff --git a/res/css/views/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss
index 5858a60629..5329e7f1f8 100644
--- a/res/css/views/elements/_InfoTooltip.scss
+++ b/res/css/views/elements/_InfoTooltip.scss
@@ -30,5 +30,12 @@ limitations under the License.
     mask-position: center;
     content: '';
     vertical-align: middle;
+}
+
+.mx_InfoTooltip_icon_info::before {
     mask-image: url('$(res)/img/element-icons/info.svg');
 }
+
+.mx_InfoTooltip_icon_warning::before {
+    mask-image: url('$(res)/img/element-icons/warning.svg');
+}
diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss
new file mode 100644
index 0000000000..54c7df3e0b
--- /dev/null
+++ b/res/css/views/messages/_CallEvent.scss
@@ -0,0 +1,154 @@
+/*
+Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+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_CallEvent {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+
+    background-color: $dark-panel-bg-color;
+    border-radius: 8px;
+    margin: 10px auto;
+    max-width: 75%;
+    box-sizing: border-box;
+    height: 60px;
+
+    &.mx_CallEvent_voice {
+        .mx_CallEvent_type_icon::before,
+        .mx_CallEvent_content_button_callBack span::before,
+        .mx_CallEvent_content_button_answer span::before {
+            mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
+        }
+    }
+
+    &.mx_CallEvent_video {
+        .mx_CallEvent_type_icon::before,
+        .mx_CallEvent_content_button_callBack span::before,
+        .mx_CallEvent_content_button_answer span::before {
+            mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+        }
+    }
+
+    .mx_CallEvent_info {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        margin-left: 12px;
+
+        .mx_CallEvent_info_basic {
+            display: flex;
+            flex-direction: column;
+            margin-left: 10px; // To match mx_CallEvent
+
+            .mx_CallEvent_sender {
+                font-weight: 600;
+                font-size: 1.5rem;
+                line-height: 1.8rem;
+                margin-bottom: 3px;
+            }
+
+            .mx_CallEvent_type {
+                font-weight: 400;
+                color: $secondary-fg-color;
+                font-size: 1.2rem;
+                line-height: $font-13px;
+                display: flex;
+                align-items: center;
+
+                .mx_CallEvent_type_icon {
+                    height: 13px;
+                    width: 13px;
+                    margin-right: 5px;
+
+                    &::before {
+                        content: '';
+                        position: absolute;
+                        height: 13px;
+                        width: 13px;
+                        background-color: $tertiary-fg-color;
+                        mask-repeat: no-repeat;
+                        mask-size: contain;
+                    }
+                }
+            }
+        }
+    }
+
+    .mx_CallEvent_content {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        color: $secondary-fg-color;
+        margin-right: 16px;
+
+        .mx_CallEvent_content_button {
+            height: 24px;
+            padding: 0px 12px;
+            margin-left: 8px;
+
+            span {
+                padding: 8px 0;
+                display: flex;
+                align-items: center;
+
+                &::before {
+                    content: '';
+                    display: inline-block;
+                    background-color: $button-fg-color;
+                    mask-position: center;
+                    mask-repeat: no-repeat;
+                    mask-size: 16px;
+                    width: 16px;
+                    height: 16px;
+                    margin-right: 8px;
+                }
+            }
+        }
+
+        .mx_CallEvent_content_button_reject span::before {
+            mask-image: url('$(res)/img/element-icons/call/hangup.svg');
+        }
+
+        .mx_CallEvent_content_tooltip {
+            margin-right: 5px;
+        }
+
+        .mx_CallEvent_iconButton {
+            display: inline-flex;
+            margin-right: 8px;
+
+            &::before {
+                content: '';
+
+                height: 16px;
+                width: 16px;
+                background-color: $tertiary-fg-color;
+                mask-repeat: no-repeat;
+                mask-size: contain;
+                mask-position: center;
+            }
+        }
+
+        .mx_CallEvent_silence::before {
+            mask-image: url('$(res)/img/voip/silence.svg');
+        }
+
+        .mx_CallEvent_unSilence::before {
+            mask-image: url('$(res)/img/voip/un-silence.svg');
+        }
+    }
+}
diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss
index 878a4154cd..0a199c1f45 100644
--- a/res/css/views/messages/_MImageBody.scss
+++ b/res/css/views/messages/_MImageBody.scss
@@ -18,7 +18,6 @@ $timelineImageBorderRadius: 4px;
 
 .mx_MImageBody {
     display: block;
-    margin-right: 34px;
 }
 
 .mx_MImageBody_thumbnail {
@@ -29,6 +28,10 @@ $timelineImageBorderRadius: 4px;
     top: 0;
     border-radius: $timelineImageBorderRadius;
 
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
     > canvas {
         border-radius: $timelineImageBorderRadius;
     }
@@ -43,17 +46,6 @@ $timelineImageBorderRadius: 4px;
     position: relative;
 }
 
-.mx_MImageBody_thumbnail_spinner {
-    position: absolute;
-    left: 50%;
-    top: 50%;
-}
-
-// Inner img should be centered around 0, 0
-.mx_MImageBody_thumbnail_spinner > * {
-    transform: translate(-50%, -50%);
-}
-
 .mx_MImageBody_gifLabel {
     position: absolute;
     display: block;
diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss
index e2fafe6c62..69f3c672b7 100644
--- a/res/css/views/messages/_MessageActionBar.scss
+++ b/res/css/views/messages/_MessageActionBar.scss
@@ -107,3 +107,12 @@ limitations under the License.
 .mx_MessageActionBar_cancelButton::after {
     mask-image: url('$(res)/img/element-icons/trashcan.svg');
 }
+
+.mx_MessageActionBar_downloadButton::after {
+    mask-size: 14px;
+    mask-image: url('$(res)/img/download.svg');
+}
+
+.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
+    background-color: transparent; // hide the download icon mask
+}
diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss
index e05065eb02..b2bca6dfb3 100644
--- a/res/css/views/messages/_ReactionsRow.scss
+++ b/res/css/views/messages/_ReactionsRow.scss
@@ -26,6 +26,7 @@ limitations under the License.
         height: 24px;
         vertical-align: middle;
         margin-left: 4px;
+        margin-right: 4px;
 
         &::before {
             content: '';
diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss
new file mode 100644
index 0000000000..c66f635ffe
--- /dev/null
+++ b/res/css/views/rooms/_EventBubbleTile.scss
@@ -0,0 +1,323 @@
+/*
+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_EventTile[data-layout=bubble],
+.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary {
+    --avatarSize: 32px;
+    --gutterSize: 11px;
+    --cornerRadius: 12px;
+    --maxWidth: 70%;
+}
+
+.mx_EventTile[data-layout=bubble] {
+
+    position: relative;
+    margin-top: var(--gutterSize);
+    margin-left: 50px;
+    margin-right: 100px;
+
+    &.mx_EventTile_continuation {
+        margin-top: 2px;
+    }
+
+    /* For replies */
+    .mx_EventTile {
+        padding-top: 0;
+    }
+
+    &:hover {
+        &::before {
+            content: '';
+            position: absolute;
+            top: -1px;
+            bottom: -1px;
+            left: -60px;
+            right: -60px;
+            z-index: -1;
+            background: $eventbubble-bg-hover;
+            border-radius: 4px;
+        }
+
+        .mx_EventTile_avatar {
+            img {
+                box-shadow: 0 0 0 3px $eventbubble-bg-hover;
+            }
+        }
+    }
+
+    .mx_SenderProfile,
+    .mx_EventTile_line {
+        width: fit-content;
+        max-width: 70%;
+    }
+
+    .mx_SenderProfile {
+        position: relative;
+        top: -2px;
+        left: 2px;
+    }
+
+    &[data-self=false] {
+        .mx_EventTile_line {
+            border-bottom-right-radius: var(--cornerRadius);
+        }
+        .mx_EventTile_avatar {
+            left: -34px;
+        }
+
+        .mx_MessageActionBar {
+            right: 0;
+            transform: translate3d(50%, 50%, 0);
+        }
+
+        --backgroundColor: $eventbubble-others-bg;
+    }
+    &[data-self=true] {
+        .mx_EventTile_line {
+            border-bottom-left-radius: var(--cornerRadius);
+            float: right;
+            > a {
+                left: auto;
+                right: -48px;
+            }
+        }
+        .mx_SenderProfile {
+            display: none;
+        }
+        .mx_ReactionsRow {
+            float: right;
+            clear: right;
+            display: flex;
+
+            /* Moving the "add reaction button" before the reactions */
+            > :last-child {
+                order: -1;
+            }
+        }
+        .mx_EventTile_avatar {
+            top: -19px; // height of the sender block
+            right: -35px;
+        }
+
+        --backgroundColor: $eventbubble-self-bg;
+    }
+
+    .mx_EventTile_line {
+        position: relative;
+        padding: var(--gutterSize);
+        border-top-left-radius: var(--cornerRadius);
+        border-top-right-radius: var(--cornerRadius);
+        background: var(--backgroundColor);
+        display: flex;
+        gap: 5px;
+        margin: 0 -12px 0 -9px;
+        > a {
+            position: absolute;
+            left: -48px;
+        }
+    }
+
+    &.mx_EventTile_continuation[data-self=false] .mx_EventTile_line {
+        border-top-left-radius: 0;
+    }
+    &.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line {
+        border-bottom-left-radius: var(--cornerRadius);
+    }
+
+    &.mx_EventTile_continuation[data-self=true] .mx_EventTile_line {
+        border-top-right-radius: 0;
+    }
+    &.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line {
+        border-bottom-right-radius: var(--cornerRadius);
+    }
+
+    .mx_EventTile_avatar {
+        position: absolute;
+        top: 0;
+        line-height: 1;
+        img {
+            box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
+            border-radius: 50%;
+        }
+    }
+
+    &[data-has-reply=true] {
+        > .mx_EventTile_line {
+            flex-direction: column;
+        }
+
+        .mx_ReplyThread_show {
+            order: 99999;
+        }
+
+        .mx_ReplyThread {
+            margin: 0 calc(-1 * var(--gutterSize));
+
+            .mx_EventTile_reply {
+                max-width: 90%;
+                padding: 0;
+                > a {
+                    display: none !important;
+                }
+            }
+
+            .mx_EventTile {
+                display: flex;
+                gap: var(--gutterSize);
+                .mx_EventTile_avatar {
+                    position: static;
+                }
+                .mx_SenderProfile {
+                    display: none;
+                }
+            }
+        }
+    }
+
+    .mx_EditMessageComposer_buttons {
+        position: static;
+        padding: 0;
+        margin: 0;
+        background: transparent;
+    }
+
+    .mx_ReactionsRow {
+        margin-right: -18px;
+        margin-left: -9px;
+    }
+
+    .mx_ReplyThread {
+        border-left-width: 2px;
+        border-left-color: $eventbubble-reply-color;
+    }
+
+    &.mx_EventTile_bubbleContainer,
+    &.mx_EventTile_info,
+    & ~ .mx_EventListSummary[data-expanded=false] {
+        --backgroundColor: transparent;
+        --gutterSize: 0;
+
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        .mx_EventTile_avatar {
+            position: static;
+            order: -1;
+            margin-right: 5px;
+        }
+    }
+
+    & ~ .mx_EventListSummary {
+        --maxWidth: 80%;
+        margin-left: calc(var(--avatarSize) + var(--gutterSize));
+        margin-right: calc(var(--gutterSize) + var(--avatarSize));
+        .mx_EventListSummary_toggle {
+            float: none;
+            margin: 0;
+            order: 9;
+            margin-left: 5px;
+        }
+        .mx_EventListSummary_avatars {
+            padding-top: 0;
+        }
+
+        &::after {
+            content: "";
+            clear: both;
+        }
+
+        .mx_EventTile {
+            margin: 0 6px;
+        }
+
+        .mx_EventTile_line {
+            margin: 0 5px;
+            > a {
+                left: auto;
+                right: 0;
+                transform: translateX(calc(100% + 5px));
+            }
+        }
+
+        .mx_MessageActionBar {
+            transform: translate3d(50%, 0, 0);
+        }
+    }
+
+    & ~ .mx_EventListSummary[data-expanded=false] {
+        padding: 0 34px;
+    }
+
+    /* events that do not require bubble layout */
+    & ~ .mx_EventListSummary,
+    &.mx_EventTile_bad {
+        .mx_EventTile_line {
+            background: transparent;
+        }
+
+        &:hover {
+            &::before {
+                background: transparent;
+            }
+        }
+    }
+
+    & + .mx_EventListSummary {
+        .mx_EventTile {
+            margin-top: 0;
+            padding: 0;
+        }
+    }
+
+    .mx_EventListSummary_toggle {
+        margin-right: 55px;
+    }
+
+    /* Special layout scenario for "Unable To Decrypt (UTD)" events */
+    &.mx_EventTile_bad > .mx_EventTile_line {
+        display: grid;
+        grid-template:
+            "reply reply" auto
+            "shield body" auto
+            "shield link" auto
+            / auto  1fr;
+        .mx_EventTile_e2eIcon {
+            grid-area: shield;
+        }
+        .mx_UnknownBody {
+            grid-area: body;
+        }
+        .mx_EventTile_keyRequestInfo {
+            grid-area: link;
+        }
+        .mx_ReplyThread_wrapper {
+            grid-area: reply;
+        }
+    }
+
+
+    .mx_EventTile_readAvatars {
+        position: absolute;
+        right: -110px;
+        bottom: 0;
+        top: auto;
+    }
+
+    .mx_MTextBody {
+        max-width: 100%;
+    }
+}
diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 55f73c0315..72328fab77 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -1,6 +1,6 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020-2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,102 +18,305 @@ limitations under the License.
 $left-gutter: 64px;
 $hover-select-border: 4px;
 
-.mx_EventTile {
+.mx_EventTile:not([data-layout=bubble]) {
     max-width: 100%;
     clear: both;
     padding-top: 18px;
     font-size: $font-14px;
     position: relative;
-}
 
-.mx_EventTile.mx_EventTile_info {
-    padding-top: 1px;
-}
+    &.mx_EventTile_info {
+        padding-top: 1px;
+    }
 
-.mx_EventTile_avatar {
-    top: 14px;
-    left: 8px;
-    cursor: pointer;
-    user-select: none;
-}
+    .mx_EventTile_avatar {
+        top: 14px;
+        left: 8px;
+        cursor: pointer;
+        user-select: none;
+    }
 
-.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
-    top: $font-6px;
-    left: $left-gutter;
-}
+    &.mx_EventTile_info .mx_EventTile_avatar {
+        top: $font-6px;
+        left: $left-gutter;
+    }
 
-.mx_EventTile_continuation {
-    padding-top: 0px !important;
+    &.mx_EventTile_continuation {
+        padding-top: 0px !important;
+
+        &.mx_EventTile_isEditing {
+            padding-top: 5px !important;
+            margin-top: -5px;
+        }
+    }
 
     &.mx_EventTile_isEditing {
-        padding-top: 5px !important;
-        margin-top: -5px;
+        background-color: $header-panel-bg-color;
     }
-}
 
-.mx_EventTile_isEditing {
-    background-color: $header-panel-bg-color;
-}
+    .mx_SenderProfile {
+        color: $primary-fg-color;
+        font-size: $font-14px;
+        display: inline-block; /* anti-zalgo, with overflow hidden */
+        overflow: hidden;
+        cursor: pointer;
+        padding-bottom: 0px;
+        padding-top: 0px;
+        margin: 0px;
+        /* the next three lines, along with overflow hidden, truncate long display names */
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        max-width: calc(100% - $left-gutter);
+    }
 
-.mx_EventTile .mx_SenderProfile {
-    color: $primary-fg-color;
-    font-size: $font-14px;
-    display: inline-block; /* anti-zalgo, with overflow hidden */
-    overflow: hidden;
-    cursor: pointer;
-    padding-bottom: 0px;
-    padding-top: 0px;
-    margin: 0px;
-    /* the next three lines, along with overflow hidden, truncate long display names */
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    max-width: calc(100% - $left-gutter);
-}
+    .mx_SenderProfile .mx_Flair {
+        opacity: 0.7;
+        margin-left: 5px;
+        display: inline-block;
+        vertical-align: top;
+        overflow: hidden;
+        user-select: none;
 
-.mx_EventTile .mx_SenderProfile .mx_Flair {
-    opacity: 0.7;
-    margin-left: 5px;
-    display: inline-block;
-    vertical-align: top;
-    overflow: hidden;
-    user-select: none;
+        img {
+            vertical-align: -2px;
+            margin-right: 2px;
+            border-radius: 8px;
+        }
+    }
 
-    img {
-        vertical-align: -2px;
-        margin-right: 2px;
+    &.mx_EventTile_isEditing .mx_MessageTimestamp {
+        visibility: hidden;
+    }
+
+    .mx_MessageTimestamp {
+        display: block;
+        white-space: nowrap;
+        left: 0px;
+        text-align: center;
+        user-select: none;
+    }
+
+    &.mx_EventTile_continuation .mx_EventTile_line {
+        clear: both;
+    }
+
+    .mx_EventTile_line, .mx_EventTile_reply {
+        position: relative;
+        padding-left: $left-gutter;
         border-radius: 8px;
     }
-}
 
-.mx_EventTile_isEditing .mx_MessageTimestamp {
-    visibility: hidden;
-}
-
-.mx_EventTile .mx_MessageTimestamp {
-    display: block;
-    white-space: nowrap;
-    left: 0px;
-    text-align: center;
-    user-select: none;
-}
-
-.mx_EventTile_continuation .mx_EventTile_line {
-    clear: both;
-}
-
-.mx_EventTile_line, .mx_EventTile_reply {
-    position: relative;
-    padding-left: $left-gutter;
-    border-radius: 8px;
-}
-
-.mx_RoomView_timeline_rr_enabled,
-// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
-.mx_EventListSummary {
-    .mx_EventTile_line {
-        /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
-        margin-right: 110px;
+    .mx_EventTile_reply {
+        margin-right: 10px;
     }
+
+    &.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
+        left: calc(-$hover-select-border);
+    }
+
+    /* this is used for the tile for the event which is selected via the URL.
+     * TODO: ultimately we probably want some transition on here.
+     */
+    &.mx_EventTile_selected > .mx_EventTile_line {
+        border-left: $accent-color 4px solid;
+        padding-left: calc($left-gutter - $hover-select-border);
+        background-color: $event-selected-color;
+    }
+
+    &.mx_EventTile_highlight,
+    &.mx_EventTile_highlight .markdown-body {
+        color: $event-highlight-fg-color;
+
+        .mx_EventTile_line {
+            background-color: $event-highlight-bg-color;
+        }
+    }
+
+    &.mx_EventTile_info .mx_EventTile_line {
+        padding-left: calc($left-gutter + 18px);
+    }
+
+    &.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
+        padding-left: calc($left-gutter + 18px - $hover-select-border);
+    }
+
+    &.mx_EventTile:hover .mx_EventTile_line,
+    &.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line,
+    &.mx_EventTile.focus-visible:focus-within .mx_EventTile_line {
+        background-color: $event-selected-color;
+    }
+
+    .mx_EventTile_searchHighlight {
+        background-color: $accent-color;
+        color: $accent-fg-color;
+        border-radius: 5px;
+        padding-left: 2px;
+        padding-right: 2px;
+        cursor: pointer;
+    }
+
+    .mx_EventTile_searchHighlight a {
+        background-color: $accent-color;
+        color: $accent-fg-color;
+    }
+
+    .mx_EventTile_receiptSent,
+    .mx_EventTile_receiptSending {
+        // We don't use `position: relative` on the element because then it won't line
+        // up with the other read receipts
+
+        &::before {
+            background-color: $tertiary-fg-color;
+            mask-repeat: no-repeat;
+            mask-position: center;
+            mask-size: 14px;
+            width: 14px;
+            height: 14px;
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+        }
+    }
+    .mx_EventTile_receiptSent::before {
+        mask-image: url('$(res)/img/element-icons/circle-sent.svg');
+    }
+    .mx_EventTile_receiptSending::before {
+        mask-image: url('$(res)/img/element-icons/circle-sending.svg');
+    }
+
+    &.mx_EventTile_contextual {
+        opacity: 0.4;
+    }
+
+    .mx_EventTile_msgOption {
+        float: right;
+        text-align: right;
+        position: relative;
+        width: 90px;
+
+        /* Hack to stop the height of this pushing the messages apart.
+           Replaces margin-top: -6px. This interacts better with a read
+           marker being in between. Content overflows. */
+        height: 1px;
+
+        margin-right: 10px;
+    }
+
+    .mx_EventTile_msgOption a {
+        text-decoration: none;
+    }
+
+    /* all the overflow-y: hidden; are to trap Zalgos -
+       but they introduce an implicit overflow-x: auto.
+       so make that explicitly hidden too to avoid random
+       horizontal scrollbars occasionally appearing, like in
+       https://github.com/vector-im/vector-web/issues/1154
+        */
+    .mx_EventTile_content {
+        display: block;
+        overflow-y: hidden;
+        overflow-x: hidden;
+        margin-right: 34px;
+    }
+
+    /* De-zalgoing */
+    .mx_EventTile_body {
+        overflow-y: hidden;
+    }
+
+    /* Spoiler stuff */
+    .mx_EventTile_spoiler {
+        cursor: pointer;
+    }
+
+    .mx_EventTile_spoiler_reason {
+        color: $event-timestamp-color;
+        font-size: $font-11px;
+    }
+
+    .mx_EventTile_spoiler_content {
+        filter: blur(5px) saturate(0.1) sepia(1);
+        transition-duration: 0.5s;
+    }
+
+    .mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
+        filter: none;
+    }
+
+    &:hover.mx_EventTile_verified .mx_EventTile_line,
+    &:hover.mx_EventTile_unverified .mx_EventTile_line,
+    &:hover.mx_EventTile_unknown .mx_EventTile_line {
+        padding-left: calc($left-gutter - $hover-select-border);
+    }
+
+    &:hover.mx_EventTile_verified .mx_EventTile_line {
+        border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
+    }
+
+    &:hover.mx_EventTile_unverified .mx_EventTile_line {
+        border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
+    }
+
+    &:hover.mx_EventTile_unknown .mx_EventTile_line {
+        border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
+    }
+
+    &:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
+    &:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
+    &:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
+        padding-left: calc($left-gutter + 18px - $hover-select-border);
+    }
+
+    /* End to end encryption stuff */
+    &:hover .mx_EventTile_e2eIcon {
+        opacity: 1;
+    }
+
+    // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
+    &:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
+    &:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
+    &:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
+        left: calc(-$hover-select-border);
+    }
+
+    // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
+    &:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
+    &:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
+    &:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
+        display: block;
+        left: 41px;
+    }
+
+    .mx_MImageBody {
+        margin-right: 34px;
+    }
+
+    .mx_EventTile_e2eIcon {
+        position: absolute;
+        top: 6px;
+        left: 44px;
+        bottom: 0;
+        right: 0;
+    }
+
+    .mx_ReactionsRow {
+        margin: 0;
+        padding: 6px 60px;
+    }
+}
+
+.mx_RoomView_timeline_rr_enabled {
+
+    .mx_EventTile:not([data-layout=bubble]) {
+        .mx_EventTile_line {
+            /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
+            margin-right: 110px;
+        }
+    }
+
+    // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
 }
 
 .mx_EventTile_bubbleContainer {
@@ -130,123 +333,15 @@ $hover-select-border: 4px;
     .mx_EventTile_msgOption {
         grid-column: 2;
     }
-}
 
-.mx_EventTile_reply {
-    margin-right: 10px;
-}
-
-/* HACK to override line-height which is already marked important elsewhere */
-.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
-    font-size: 48px !important;
-    line-height: 57px !important;
-}
-
-.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
-    left: calc(-$hover-select-border);
-}
-
-.mx_EventTile:hover .mx_MessageActionBar,
-.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
-[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
-.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
-    visibility: visible;
-}
-
-/* this is used for the tile for the event which is selected via the URL.
- * TODO: ultimately we probably want some transition on here.
- */
-.mx_EventTile_selected > .mx_EventTile_line {
-    border-left: $accent-color 4px solid;
-    padding-left: calc($left-gutter - $hover-select-border);
-    background-color: $event-selected-color;
-}
-
-.mx_EventTile_highlight,
-.mx_EventTile_highlight .markdown-body {
-    color: $event-highlight-fg-color;
-
-    .mx_EventTile_line {
-        background-color: $event-highlight-bg-color;
+    &:hover {
+        .mx_EventTile_line {
+            // To avoid bubble events being highlighted
+            background-color: inherit !important;
+        }
     }
 }
 
-.mx_EventTile_info .mx_EventTile_line {
-    padding-left: calc($left-gutter + 18px);
-}
-
-.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
-    padding-left: calc($left-gutter + 18px - $hover-select-border);
-}
-
-.mx_EventTile:hover .mx_EventTile_line,
-.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line,
-.mx_EventTile.focus-visible:focus-within .mx_EventTile_line {
-    background-color: $event-selected-color;
-}
-
-.mx_EventTile_searchHighlight {
-    background-color: $accent-color;
-    color: $accent-fg-color;
-    border-radius: 5px;
-    padding-left: 2px;
-    padding-right: 2px;
-    cursor: pointer;
-}
-
-.mx_EventTile_searchHighlight a {
-    background-color: $accent-color;
-    color: $accent-fg-color;
-}
-
-.mx_EventTile_receiptSent,
-.mx_EventTile_receiptSending {
-    // We don't use `position: relative` on the element because then it won't line
-    // up with the other read receipts
-
-    &::before {
-        background-color: $tertiary-fg-color;
-        mask-repeat: no-repeat;
-        mask-position: center;
-        mask-size: 14px;
-        width: 14px;
-        height: 14px;
-        content: '';
-        position: absolute;
-        top: 0;
-        left: 0;
-        right: 0;
-    }
-}
-.mx_EventTile_receiptSent::before {
-    mask-image: url('$(res)/img/element-icons/circle-sent.svg');
-}
-.mx_EventTile_receiptSending::before {
-    mask-image: url('$(res)/img/element-icons/circle-sending.svg');
-}
-
-.mx_EventTile_contextual {
-    opacity: 0.4;
-}
-
-.mx_EventTile_msgOption {
-    float: right;
-    text-align: right;
-    position: relative;
-    width: 90px;
-
-    /* Hack to stop the height of this pushing the messages apart.
-       Replaces margin-top: -6px. This interacts better with a read
-       marker being in between. Content overflows. */
-    height: 1px;
-
-    margin-right: 10px;
-}
-
-.mx_EventTile_msgOption a {
-    text-decoration: none;
-}
-
 .mx_EventTile_readAvatars {
     position: relative;
     display: inline-block;
@@ -277,52 +372,27 @@ $hover-select-border: 4px;
     position: absolute;
 }
 
-/* all the overflow-y: hidden; are to trap Zalgos -
-   but they introduce an implicit overflow-x: auto.
-   so make that explicitly hidden too to avoid random
-   horizontal scrollbars occasionally appearing, like in
-   https://github.com/vector-im/vector-web/issues/1154
-    */
-.mx_EventTile_content {
-    display: block;
-    overflow-y: hidden;
-    overflow-x: hidden;
-    margin-right: 34px;
+/* HACK to override line-height which is already marked important elsewhere */
+.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
+    font-size: 48px !important;
+    line-height: 57px !important;
 }
 
-/* De-zalgoing */
-.mx_EventTile_body {
-    overflow-y: hidden;
-}
-
-/* Spoiler stuff */
-.mx_EventTile_spoiler {
+.mx_EventTile_content .mx_EventTile_edited {
+    user-select: none;
+    font-size: $font-12px;
+    color: $roomtopic-color;
+    display: inline-block;
+    margin-left: 9px;
     cursor: pointer;
 }
 
-.mx_EventTile_spoiler_reason {
-    color: $event-timestamp-color;
-    font-size: $font-11px;
-}
-
-.mx_EventTile_spoiler_content {
-    filter: blur(5px) saturate(0.1) sepia(1);
-    transition-duration: 0.5s;
-}
-
-.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
-    filter: none;
-}
 
 .mx_EventTile_e2eIcon {
-    position: absolute;
-    top: 6px;
-    left: 44px;
+    position: relative;
     width: 14px;
     height: 14px;
     display: block;
-    bottom: 0;
-    right: 0;
     opacity: 0.2;
     background-repeat: no-repeat;
     background-size: contain;
@@ -381,87 +451,6 @@ $hover-select-border: 4px;
     opacity: 1;
 }
 
-.mx_EventTile_keyRequestInfo {
-    font-size: $font-12px;
-}
-
-.mx_EventTile_keyRequestInfo_text {
-    opacity: 0.5;
-}
-
-.mx_EventTile_keyRequestInfo_text a {
-    color: $primary-fg-color;
-    text-decoration: underline;
-    cursor: pointer;
-}
-
-.mx_EventTile_keyRequestInfo_tooltip_contents p {
-    text-align: auto;
-    margin-left: 3px;
-    margin-right: 3px;
-}
-
-.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
-    margin-top: 0px;
-}
-
-.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
-    margin-bottom: 0px;
-}
-
-.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: calc($left-gutter - $hover-select-border);
-}
-
-.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
-    border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
-}
-
-.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
-    border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
-}
-
-.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
-    border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
-}
-
-.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: calc($left-gutter + 18px - $hover-select-border);
-}
-
-/* End to end encryption stuff */
-.mx_EventTile:hover .mx_EventTile_e2eIcon {
-    opacity: 1;
-}
-
-// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
-.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 {
-    left: calc(-$hover-select-border);
-}
-
-// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
-.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
-.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
-.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
-    display: block;
-    left: 41px;
-}
-
-.mx_EventTile_content .mx_EventTile_edited {
-    user-select: none;
-    font-size: $font-12px;
-    color: $roomtopic-color;
-    display: inline-block;
-    margin-left: 9px;
-    cursor: pointer;
-}
-
 /* Various markdown overrides */
 
 .mx_EventTile_body pre {
@@ -595,6 +584,35 @@ $hover-select-border: 4px;
 
 /* end of overrides */
 
+
+.mx_EventTile_keyRequestInfo {
+    font-size: $font-12px;
+}
+
+.mx_EventTile_keyRequestInfo_text {
+    opacity: 0.5;
+}
+
+.mx_EventTile_keyRequestInfo_text a {
+    color: $primary-fg-color;
+    text-decoration: underline;
+    cursor: pointer;
+}
+
+.mx_EventTile_keyRequestInfo_tooltip_contents p {
+    text-align: auto;
+    margin-left: 3px;
+    margin-right: 3px;
+}
+
+.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
+    margin-top: 0px;
+}
+
+.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
+    margin-bottom: 0px;
+}
+
 .mx_EventTile_tileError {
     color: red;
     text-align: center;
@@ -615,6 +633,13 @@ $hover-select-border: 4px;
     }
 }
 
+.mx_EventTile:hover .mx_MessageActionBar,
+.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
+[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
+.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
+    visibility: visible;
+}
+
 @media only screen and (max-width: 480px) {
     .mx_EventTile_line, .mx_EventTile_reply {
         padding-left: 0;
diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2
index a52e5a3800..128aac8139 100644
Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 differ
diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2
index 660a93193d..a95e89c094 100644
Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 differ
diff --git a/res/img/element-icons/warning.svg b/res/img/element-icons/warning.svg
new file mode 100644
index 0000000000..eef5193140
--- /dev/null
+++ b/res/img/element-icons/warning.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM6.9806 4.5101C6.9306 3.9401 7.3506 3.4401 7.9206 3.4001C8.4806 3.3601 8.9806 3.7801 9.0406 4.3501V4.5101L8.7206 8.5101C8.6906 8.8801 8.3806 9.1601 8.0106 9.1601H7.9506C7.6006 9.1301 7.3306 8.8601 7.3006 8.5101L6.9806 4.5101ZM8.88012 11.1202C8.88012 11.6062 8.48613 12.0002 8.00012 12.0002C7.51411 12.0002 7.12012 11.6062 7.12012 11.1202C7.12012 10.6342 7.51411 10.2402 8.00012 10.2402C8.48613 10.2402 8.88012 10.6342 8.88012 11.1202Z" fill="#8D99A5"/>
+</svg>
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 74b33fbd02..2a4ebff034 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -227,6 +227,13 @@ $groupFilterPanel-background-blur-amount: 30px;
 
 $composer-shadow-color: rgba(0, 0, 0, 0.28);
 
+// Bubble tiles
+$eventbubble-self-bg: #143A34;
+$eventbubble-others-bg: #394049;
+$eventbubble-bg-hover: #433C23;
+$eventbubble-avatar-outline: $bg-color;
+$eventbubble-reply-color: #C1C6CD;
+
 // ***** Mixins! *****
 
 @define-mixin mx_DialogButton {
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index c7debcdabe..f349a804a8 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -347,6 +347,13 @@ $appearance-tab-border-color: $input-darker-bg-color;
 
 $composer-shadow-color: tranparent;
 
+// Bubble tiles
+$eventbubble-self-bg: #F8FDFC;
+$eventbubble-others-bg: #F7F8F9;
+$eventbubble-bg-hover: rgb(242, 242, 242);
+$eventbubble-avatar-outline: #fff;
+$eventbubble-reply-color: #C1C6CD;
+
 // ***** Mixins! *****
 
 @define-mixin mx_DialogButton {
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 7e958c2af6..ef5f4d8c86 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -349,6 +349,13 @@ $groupFilterPanel-background-blur-amount: 20px;
 
 $composer-shadow-color: rgba(0, 0, 0, 0.04);
 
+// Bubble tiles
+$eventbubble-self-bg: #F8FDFC;
+$eventbubble-others-bg: #F7F8F9;
+$eventbubble-bg-hover: #FEFCF5;
+$eventbubble-avatar-outline: $primary-bg-color;
+$eventbubble-reply-color: #C1C6CD;
+
 // ***** Mixins! *****
 
 @define-mixin mx_DialogButton {
diff --git a/src/@types/common.ts b/src/@types/common.ts
index 1fb9ba4303..36ef7a9ace 100644
--- a/src/@types/common.ts
+++ b/src/@types/common.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { JSXElementConstructor } from "react";
+import React, { JSXElementConstructor } from "react";
 
 // Based on https://stackoverflow.com/a/53229857/3532235
 export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
@@ -22,3 +22,4 @@ export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<
 export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
 
 export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
+export type ReactAnyComponent = React.Component | React.ExoticComponent;
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 50cfa745a4..7f2ae91933 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -50,6 +50,8 @@ import UIStore from "../stores/UIStore";
 import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
 import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
 
+/* eslint-disable @typescript-eslint/naming-convention */
+
 declare global {
     interface Window {
         matrixChat: ReturnType<Renderer>;
@@ -91,8 +93,7 @@ declare global {
         mxSetupEncryptionStore?: SetupEncryptionStore;
         mxRoomScrollStateStore?: RoomScrollStateStore;
         grecaptcha: any;
-        // eslint-disable-next-line
-        mx_on_recaptcha_loaded: () => void;
+        mxOnRecaptchaLoaded?: () => void;
     }
 
     interface Document {
@@ -117,7 +118,7 @@ declare global {
     }
 
     interface StorageEstimate {
-        usageDetails?: {[key: string]: number};
+        usageDetails?: { [key: string]: number };
     }
 
     interface HTMLAudioElement {
@@ -188,4 +189,21 @@ declare global {
             parameterDescriptors?: AudioParamDescriptor[];
         }
     );
+
+    // eslint-disable-next-line no-var
+    var grecaptcha:
+        | undefined
+        | {
+              reset: (id: string) => void;
+              render: (
+                  divId: string,
+                  options: {
+                      sitekey: string;
+                      callback: (response: string) => void;
+                  },
+              ) => string;
+              isReady: () => boolean;
+          };
 }
+
+/* eslint-enable @typescript-eslint/naming-convention */
diff --git a/src/Analytics.tsx b/src/Analytics.tsx
index ce8287de56..fc4664039f 100644
--- a/src/Analytics.tsx
+++ b/src/Analytics.tsx
@@ -270,7 +270,7 @@ export class Analytics {
         localStorage.removeItem(LAST_VISIT_TS_KEY);
     }
 
-    private async _track(data: IData) {
+    private async track(data: IData) {
         if (this.disabled) return;
 
         const now = new Date();
@@ -304,7 +304,7 @@ export class Analytics {
     }
 
     public ping() {
-        this._track({
+        this.track({
             ping: "1",
         });
         localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts
@@ -324,14 +324,14 @@ export class Analytics {
             // But continue anyway because we still want to track the change
         }
 
-        this._track({
+        this.track({
             gt_ms: String(generationTimeMs),
         });
     }
 
     public trackEvent(category: string, action: string, name?: string, value?: string) {
         if (this.disabled) return;
-        this._track({
+        this.track({
             e_c: category,
             e_a: action,
             e_n: name,
@@ -395,17 +395,17 @@ export class Analytics {
         Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
             title: _t('Analytics'),
             description: <div className="mx_AnalyticsModal">
-                <div>{_t('The information being sent to us to help make %(brand)s better includes:', {
+                <div>{ _t('The information being sent to us to help make %(brand)s better includes:', {
                     brand: SdkConfig.get().brand,
-                })}</div>
+                }) }</div>
                 <table>
                     { rows.map((row) => <tr key={row[0]}>
-                        <td>{_t(
+                        <td>{ _t(
                             customVariables[row[0]].expl,
                             customVariables[row[0]].getTextVariables ?
                                 customVariables[row[0]].getTextVariables() :
                                 null,
-                        )}</td>
+                        ) }</td>
                         { row[1] !== undefined && <td><code>{ row[1] }</code></td> }
                     </tr>) }
                     { otherVariables.map((item, index) =>
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index f90854ee64..e7ba1aa9fb 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
 // (and store the ID of their native room)
 export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
 
-export enum AudioID {
+enum AudioID {
     Ring = 'ringAudio',
     Ringback = 'ringbackAudio',
     CallEnd = 'callendAudio',
@@ -142,6 +142,7 @@ export enum PlaceCallType {
 export enum CallHandlerEvent {
     CallsChanged = "calls_changed",
     CallChangeRoom = "call_change_room",
+    SilencedCallsChanged = "silenced_calls_changed",
 }
 
 export default class CallHandler extends EventEmitter {
@@ -164,6 +165,8 @@ export default class CallHandler extends EventEmitter {
     // do the async lookup when we get new information and then store these mappings here
     private assertedIdentityNativeUsers = new Map<string, string>();
 
+    private silencedCalls = new Set<string>(); // callIds
+
     static sharedInstance() {
         if (!window.mxCallHandler) {
             window.mxCallHandler = new CallHandler();
@@ -224,6 +227,33 @@ export default class CallHandler extends EventEmitter {
         }
     }
 
+    public silenceCall(callId: string) {
+        this.silencedCalls.add(callId);
+        this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
+
+        // Don't pause audio if we have calls which are still ringing
+        if (this.areAnyCallsUnsilenced()) return;
+        this.pause(AudioID.Ring);
+    }
+
+    public unSilenceCall(callId: string) {
+        this.silencedCalls.delete(callId);
+        this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
+        this.play(AudioID.Ring);
+    }
+
+    public isCallSilenced(callId: string): boolean {
+        return this.silencedCalls.has(callId);
+    }
+
+    /**
+     * Returns true if there is at least one unsilenced call
+     * @returns {boolean}
+     */
+    private areAnyCallsUnsilenced(): boolean {
+        return this.calls.size > this.silencedCalls.size;
+    }
+
     private async checkProtocols(maxTries) {
         try {
             const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
@@ -301,6 +331,13 @@ export default class CallHandler extends EventEmitter {
         }, true);
     };
 
+    public getCallById(callId: string): MatrixCall {
+        for (const call of this.calls.values()) {
+            if (call.callId === callId) return call;
+        }
+        return null;
+    }
+
     getCallForRoom(roomId: string): MatrixCall {
         return this.calls.get(roomId) || null;
     }
@@ -441,6 +478,10 @@ export default class CallHandler extends EventEmitter {
                     break;
             }
 
+            if (newState !== CallState.Ringing) {
+                this.silencedCalls.delete(call.callId);
+            }
+
             switch (newState) {
                 case CallState.Ringing:
                     this.play(AudioID.Ring);
@@ -615,23 +656,23 @@ export default class CallHandler extends EventEmitter {
 
     private showICEFallbackPrompt() {
         const cli = MatrixClientPeg.get();
-        const code = sub => <code>{sub}</code>;
+        const code = sub => <code>{ sub }</code>;
         Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
             title: _t("Call failed due to misconfigured server"),
             description: <div>
-                <p>{_t(
+                <p>{ _t(
                     "Please ask the administrator of your homeserver " +
                     "(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
                     "order for calls to work reliably.",
                     { homeserverDomain: cli.getDomain() }, { code },
-                )}</p>
-                <p>{_t(
+                ) }</p>
+                <p>{ _t(
                     "Alternatively, you can try to use the public server at " +
                     "<code>turn.matrix.org</code>, but this will not be as reliable, and " +
                     "it will share your IP address with that server. You can also manage " +
                     "this in Settings.",
                     null, { code },
-                )}</p>
+                ) }</p>
             </div>,
             button: _t('Try using turn.matrix.org'),
             cancelButton: _t('OK'),
@@ -649,19 +690,19 @@ export default class CallHandler extends EventEmitter {
         if (call.type === CallType.Voice) {
             title = _t("Unable to access microphone");
             description = <div>
-                {_t(
+                { _t(
                     "Call failed because microphone could not be accessed. " +
                     "Check that a microphone is plugged in and set up correctly.",
-                )}
+                ) }
             </div>;
         } else if (call.type === CallType.Video) {
             title = _t("Unable to access webcam / microphone");
             description = <div>
-                {_t("Call failed because webcam or microphone could not be accessed. Check that:")}
+                { _t("Call failed because webcam or microphone could not be accessed. Check that:") }
                 <ul>
-                    <li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
-                    <li>{_t("Permission is granted to use the webcam")}</li>
-                    <li>{_t("No other application is using the webcam")}</li>
+                    <li>{ _t("A microphone and webcam are plugged in and set up correctly") }</li>
+                    <li>{ _t("Permission is granted to use the webcam") }</li>
+                    <li>{ _t("No other application is using the webcam") }</li>
                 </ul>
             </div>;
         }
diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx
index 0c65a7bd35..c5bcb226ff 100644
--- a/src/ContentMessages.tsx
+++ b/src/ContentMessages.tsx
@@ -425,10 +425,10 @@ export default class ContentMessages {
             const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
                 title: _t('Replying With Files'),
                 description: (
-                    <div>{_t(
+                    <div>{ _t(
                         'At this time it is not possible to reply with a file. ' +
                         'Would you like to upload this file without replying?',
-                    )}</div>
+                    ) }</div>
                 ),
                 hasCancelButton: true,
                 button: _t("Continue"),
diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts
index d033063677..51c624e3c3 100644
--- a/src/DeviceListener.ts
+++ b/src/DeviceListener.ts
@@ -33,6 +33,7 @@ import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityMan
 import { isSecureBackupRequired } from './utils/WellKnownUtils';
 import { isLoggedIn } from './components/structures/MatrixChat';
 import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { ActionPayload } from "./dispatcher/payloads";
 
 const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
 
@@ -58,28 +59,28 @@ export default class DeviceListener {
     }
 
     start() {
-        MatrixClientPeg.get().on('crypto.willUpdateDevices', this._onWillUpdateDevices);
-        MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
-        MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
-        MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
-        MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
-        MatrixClientPeg.get().on('accountData', this._onAccountData);
-        MatrixClientPeg.get().on('sync', this._onSync);
-        MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents);
-        this.dispatcherRef = dis.register(this._onAction);
-        this._recheck();
+        MatrixClientPeg.get().on('crypto.willUpdateDevices', this.onWillUpdateDevices);
+        MatrixClientPeg.get().on('crypto.devicesUpdated', this.onDevicesUpdated);
+        MatrixClientPeg.get().on('deviceVerificationChanged', this.onDeviceVerificationChanged);
+        MatrixClientPeg.get().on('userTrustStatusChanged', this.onUserTrustStatusChanged);
+        MatrixClientPeg.get().on('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
+        MatrixClientPeg.get().on('accountData', this.onAccountData);
+        MatrixClientPeg.get().on('sync', this.onSync);
+        MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
+        this.dispatcherRef = dis.register(this.onAction);
+        this.recheck();
     }
 
     stop() {
         if (MatrixClientPeg.get()) {
-            MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this._onWillUpdateDevices);
-            MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
-            MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
-            MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
-            MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
-            MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
-            MatrixClientPeg.get().removeListener('sync', this._onSync);
-            MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents);
+            MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this.onWillUpdateDevices);
+            MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this.onDevicesUpdated);
+            MatrixClientPeg.get().removeListener('deviceVerificationChanged', this.onDeviceVerificationChanged);
+            MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged);
+            MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
+            MatrixClientPeg.get().removeListener('accountData', this.onAccountData);
+            MatrixClientPeg.get().removeListener('sync', this.onSync);
+            MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
         }
         if (this.dispatcherRef) {
             dis.unregister(this.dispatcherRef);
@@ -103,15 +104,15 @@ export default class DeviceListener {
             this.dismissed.add(d);
         }
 
-        this._recheck();
+        this.recheck();
     }
 
     dismissEncryptionSetup() {
         this.dismissedThisDeviceToast = true;
-        this._recheck();
+        this.recheck();
     }
 
-    _ensureDeviceIdsAtStartPopulated() {
+    private ensureDeviceIdsAtStartPopulated() {
         if (this.ourDeviceIdsAtStart === null) {
             const cli = MatrixClientPeg.get();
             this.ourDeviceIdsAtStart = new Set(
@@ -120,39 +121,39 @@ export default class DeviceListener {
         }
     }
 
-    _onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
+    private onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
         // If we didn't know about *any* devices before (ie. it's fresh login),
         // then they are all pre-existing devices, so ignore this and set the
         // devicesAtStart list to the devices that we see after the fetch.
         if (initialFetch) return;
 
         const myUserId = MatrixClientPeg.get().getUserId();
-        if (users.includes(myUserId)) this._ensureDeviceIdsAtStartPopulated();
+        if (users.includes(myUserId)) this.ensureDeviceIdsAtStartPopulated();
 
         // No need to do a recheck here: we just need to get a snapshot of our devices
         // before we download any new ones.
     };
 
-    _onDevicesUpdated = (users: string[]) => {
+    private onDevicesUpdated = (users: string[]) => {
         if (!users.includes(MatrixClientPeg.get().getUserId())) return;
-        this._recheck();
+        this.recheck();
     };
 
-    _onDeviceVerificationChanged = (userId: string) => {
+    private onDeviceVerificationChanged = (userId: string) => {
         if (userId !== MatrixClientPeg.get().getUserId()) return;
-        this._recheck();
+        this.recheck();
     };
 
-    _onUserTrustStatusChanged = (userId: string) => {
+    private onUserTrustStatusChanged = (userId: string) => {
         if (userId !== MatrixClientPeg.get().getUserId()) return;
-        this._recheck();
+        this.recheck();
     };
 
-    _onCrossSingingKeysChanged = () => {
-        this._recheck();
+    private onCrossSingingKeysChanged = () => {
+        this.recheck();
     };
 
-    _onAccountData = (ev) => {
+    private onAccountData = (ev: MatrixEvent) => {
         // User may have:
         // * migrated SSSS to symmetric
         // * uploaded keys to secret storage
@@ -163,32 +164,32 @@ export default class DeviceListener {
             ev.getType().startsWith('m.cross_signing.') ||
             ev.getType() === 'm.megolm_backup.v1'
         ) {
-            this._recheck();
+            this.recheck();
         }
     };
 
-    _onSync = (state, prevState) => {
-        if (state === 'PREPARED' && prevState === null) this._recheck();
+    private onSync = (state, prevState) => {
+        if (state === 'PREPARED' && prevState === null) this.recheck();
     };
 
-    _onRoomStateEvents = (ev: MatrixEvent) => {
+    private onRoomStateEvents = (ev: MatrixEvent) => {
         if (ev.getType() !== "m.room.encryption") {
             return;
         }
 
         // If a room changes to encrypted, re-check as it may be our first
         // encrypted room. This also catches encrypted room creation as well.
-        this._recheck();
+        this.recheck();
     };
 
-    _onAction = ({ action }) => {
+    private onAction = ({ action }: ActionPayload) => {
         if (action !== "on_logged_in") return;
-        this._recheck();
+        this.recheck();
     };
 
     // The server doesn't tell us when key backup is set up, so we poll
     // & cache the result
-    async _getKeyBackupInfo() {
+    private async getKeyBackupInfo() {
         const now = (new Date()).getTime();
         if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
             this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
@@ -206,7 +207,7 @@ export default class DeviceListener {
         return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
     }
 
-    async _recheck() {
+    private async recheck() {
         const cli = MatrixClientPeg.get();
 
         if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
@@ -235,7 +236,7 @@ export default class DeviceListener {
                 // Cross-signing on account but this device doesn't trust the master key (verify this session)
                 showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
             } else {
-                const backupInfo = await this._getKeyBackupInfo();
+                const backupInfo = await this.getKeyBackupInfo();
                 if (backupInfo) {
                     // No cross-signing on account but key backup available (upgrade encryption)
                     showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
@@ -256,7 +257,7 @@ export default class DeviceListener {
 
         // This needs to be done after awaiting on downloadKeys() above, so
         // we make sure we get the devices after the fetch is done.
-        this._ensureDeviceIdsAtStartPopulated();
+        this.ensureDeviceIdsAtStartPopulated();
 
         // Unverified devices that were there last time the app ran
         // (technically could just be a boolean: we don't actually
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index a37b7f0ac9..3c34bf6837 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -33,7 +33,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html';
 import linkifyMatrix from './linkify-matrix';
 import SettingsStore from './settings/SettingsStore';
 import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
-import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji";
+import { getEmojiFromUnicode } from "./emoji";
 import ReplyThread from "./components/views/elements/ReplyThread";
 import { mediaFromMxc } from "./customisations/Media";
 
@@ -79,20 +79,8 @@ function mightContainEmoji(str: string): boolean {
  * @return {String} The shortcode (such as :thumbup:)
  */
 export function unicodeToShortcode(char: string): string {
-    const data = getEmojiFromUnicode(char);
-    return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
-}
-
-/**
- * Returns the unicode character for an emoji shortcode
- *
- * @param {String} shortcode The shortcode (such as :thumbup:)
- * @return {String} The emoji character; null if none exists
- */
-export function shortcodeToUnicode(shortcode: string): string {
-    shortcode = shortcode.slice(1, shortcode.length - 1);
-    const data = SHORTCODE_TO_EMOJI.get(shortcode);
-    return data ? data.unicode : null;
+    const shortcodes = getEmojiFromUnicode(char).shortcodes;
+    return shortcodes.length > 0 ? `:${shortcodes[0]}:` : '';
 }
 
 export function processHtmlForSending(html: string): string {
diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js
index 447c5edd30..e91e1d72cf 100644
--- a/src/IdentityAuthClient.js
+++ b/src/IdentityAuthClient.js
@@ -149,17 +149,17 @@ export default class IdentityAuthClient {
                 title: _t("Identity server has no terms of service"),
                 description: (
                     <div>
-                        <p>{_t(
+                        <p>{ _t(
                             "This action requires accessing the default identity server " +
                             "<server /> to validate an email address or phone number, " +
                             "but the server does not have any terms of service.", {},
                             {
-                                server: () => <b>{abbreviateUrl(identityServerUrl)}</b>,
+                                server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
                             },
-                        )}</p>
-                        <p>{_t(
+                        ) }</p>
+                        <p>{ _t(
                             "Only continue if you trust the owner of the server.",
-                        )}</p>
+                        ) }</p>
                     </div>
                 ),
                 button: _t("Trust"),
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index e9364b1b47..f43351aab2 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -105,7 +105,7 @@ export interface IMatrixClientPeg {
  * This module provides a singleton instance of this class so the 'current'
  * Matrix Client object is available easily.
  */
-class _MatrixClientPeg implements IMatrixClientPeg {
+class MatrixClientPegClass implements IMatrixClientPeg {
     // These are the default options used when when the
     // client is started in 'start'. These can be altered
     // at any time up to after the 'will_start_client'
@@ -300,7 +300,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
 }
 
 if (!window.mxMatrixClientPeg) {
-    window.mxMatrixClientPeg = new _MatrixClientPeg();
+    window.mxMatrixClientPeg = new MatrixClientPegClass();
 }
 
 export const MatrixClientPeg = window.mxMatrixClientPeg;
diff --git a/src/Modal.tsx b/src/Modal.tsx
index 55fc871d67..1e84078ddb 100644
--- a/src/Modal.tsx
+++ b/src/Modal.tsx
@@ -378,7 +378,7 @@ export class ModalManager {
             const dialog = (
                 <div className={classes}>
                     <div className="mx_Dialog">
-                        {modal.elem}
+                        { modal.elem }
                     </div>
                     <div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
                 </div>
diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 7753ff6f75..9f5ac83a56 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -480,14 +480,14 @@ export const Commands = [
                                 'Identity server',
                                 QuestionDialog, {
                                     title: _t("Use an identity server"),
-                                    description: <p>{_t(
+                                    description: <p>{ _t(
                                         "Use an identity server to invite by email. " +
                                         "Click continue to use the default identity server " +
                                         "(%(defaultIdentityServerName)s) or manage in Settings.",
                                         {
                                             defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
                                         },
-                                    )}</p>,
+                                    ) }</p>,
                                     button: _t("Continue"),
                                 },
                             );
@@ -522,7 +522,7 @@ export const Commands = [
         aliases: ['j', 'goto'],
         args: '<room-address>',
         description: _td('Joins room with given address'),
-        runFn: function(_, args) {
+        runFn: function(roomId, args) {
             if (args) {
                 // Note: we support 2 versions of this command. The first is
                 // the public-facing one for most users and the other is a
@@ -1069,7 +1069,7 @@ export const Commands = [
         command: "msg",
         description: _td("Sends a message to the given user"),
         args: "<user-id> <message>",
-        runFn: function(_, args) {
+        runFn: function(roomId, args) {
             if (args) {
                 // matches the first whitespace delimited group and then the rest of the string
                 const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx
index 0056a37c85..7bad8eb50e 100644
--- a/src/TextForEvent.tsx
+++ b/src/TextForEvent.tsx
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 import React from 'react';
-import { MatrixClientPeg } from './MatrixClientPeg';
 import { _t } from './languageHandler';
 import * as Roles from './Roles';
 import { isValid3pidInvite } from "./RoomInvite";
@@ -318,90 +317,6 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
     });
 }
 
-function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
-    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: MatrixEvent): () => string | null {
-    const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
-    const eventContent = event.getContent();
-    let getReason = () => "";
-    if (!MatrixClientPeg.get().supportsVoip()) {
-        getReason = () => _t('(not supported by this browser)');
-    } else if (eventContent.reason) {
-        if (eventContent.reason === "ice_failed") {
-            // We couldn't establish a connection at all
-            getReason = () => _t('(could not connect media)');
-        } else if (eventContent.reason === "ice_timeout") {
-            // We established a connection but it died
-            getReason = () => _t('(connection failed)');
-        } else if (eventContent.reason === "user_media_failed") {
-            // The other side couldn't open capture devices
-            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)
-            getReason = () => _t("(an error occurred)");
-        } else if (eventContent.reason === "invite_timeout") {
-            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)
-            getReason = () => '';
-        } else {
-            getReason = () => _t('(unknown failure: %(reason)s)', { reason: eventContent.reason });
-        }
-    }
-    return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
-}
-
-function textForCallRejectEvent(event: MatrixEvent): () => string | null {
-    return () => {
-        const senderName = event.sender ? event.sender.name : _t('Someone');
-        return _t('%(senderName)s declined the call.', { senderName });
-    };
-}
-
-function textForCallInviteEvent(event: MatrixEvent): () => 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;
-    if (event.getContent().offer && event.getContent().offer.sdp &&
-            event.getContent().offer.sdp.indexOf('m=video') !== -1) {
-        isVoice = false;
-    }
-    const isSupported = MatrixClientPeg.get().supportsVoip();
-
-    // This ladder could be reduced down to a couple string variables, however other languages
-    // 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: getSenderName(),
-        });
-    } else if (isVoice && !isSupported) {
-        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: getSenderName(),
-        });
-    } else if (!isVoice && !isSupported) {
-        return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
-            senderName: getSenderName(),
-        });
-    }
-}
-
 function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
     const senderName = event.sender ? event.sender.name : event.getSender();
 
@@ -652,10 +567,6 @@ interface IHandlers {
 
 const handlers: IHandlers = {
     'm.room.message': textForMessageEvent,
-    'm.call.invite': textForCallInviteEvent,
-    'm.call.answer': textForCallAnswerEvent,
-    'm.call.hangup': textForCallHangupEvent,
-    'm.call.reject': textForCallRejectEvent,
 };
 
 const stateHandlers: IHandlers = {
diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx
index c5cf85facd..9cc7b60c99 100644
--- a/src/accessibility/KeyboardShortcuts.tsx
+++ b/src/accessibility/KeyboardShortcuts.tsx
@@ -370,8 +370,8 @@ export const toggleDialog = () => {
     const sections = categoryOrder.map(category => {
         const list = shortcuts[category];
         return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
-            <h3>{_t(category)}</h3>
-            <div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div>
+            <h3>{ _t(category) }</h3>
+            <div>{ list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />) }</div>
         </div>;
     });
 
diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx
index 8d882fadea..90538760bb 100644
--- a/src/accessibility/Toolbar.tsx
+++ b/src/accessibility/Toolbar.tsx
@@ -62,9 +62,9 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
     };
 
     return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
-        {({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
+        { ({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
             { children }
-        </div>}
+        </div> }
     </RovingTabIndexProvider>;
 };
 
diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx
index 3088cbfdf4..4d8f5e5663 100644
--- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx
+++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx
@@ -59,8 +59,8 @@ export default class DisableEventIndexDialog extends React.Component<IProps, ISt
     public render(): React.ReactNode {
         return (
             <BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
-                {_t("If disabled, messages from encrypted rooms won't appear in search results.")}
-                {this.state.disabling ? <Spinner /> : <div />}
+                { _t("If disabled, messages from encrypted rooms won't appear in search results.") }
+                { this.state.disabling ? <Spinner /> : <div /> }
                 <DialogButtons
                     primaryButton={_t('Disable')}
                     onPrimaryButtonClick={this.onDisable}
diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
index 8a11ff4b1b..2748fda35a 100644
--- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
+++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
@@ -162,19 +162,19 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
 
         const eventIndexingSettings = (
             <div>
-                {_t(
+                { _t(
                     "%(brand)s is securely caching encrypted messages locally for them " +
                     "to appear in search results:",
                     { brand },
-                )}
+                ) }
                 <div className='mx_SettingsTab_subsectionText'>
-                    {crawlerState}<br />
-                    {_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
-                    {_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
-                    {_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", {
+                    { crawlerState }<br />
+                    { _t("Space used:") } { formatBytes(this.state.eventIndexSize, 0) }<br />
+                    { _t("Indexed messages:") } { formatCountLong(this.state.eventCount) }<br />
+                    { _t("Indexed rooms:") } { _t("%(doneRooms)s out of %(totalRooms)s", {
                         doneRooms: formatCountLong(doneRooms),
                         totalRooms: formatCountLong(this.state.roomCount),
-                    })} <br />
+                    }) } <br />
                     <Field
                         label={_t('Message downloading sleep time(ms)')}
                         type='number'
@@ -189,7 +189,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
                 onFinished={this.props.onFinished}
                 title={_t("Message search")}
             >
-                {eventIndexingSettings}
+                { eventIndexingSettings }
                 <DialogButtons
                     primaryButton={_t("Done")}
                     onPrimaryButtonClick={this.props.onFinished}
diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
index 92fb37ef16..412194ab43 100644
--- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
+++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
@@ -232,15 +232,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
         const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
 
         return <form onSubmit={this._onPassPhraseNextClick}>
-            <p>{_t(
+            <p>{ _t(
                 "<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
-                { b: sub => <b>{sub}</b> },
-            )}</p>
-            <p>{_t(
+                { b: sub => <b>{ sub }</b> },
+            ) }</p>
+            <p>{ _t(
                 "We'll store an encrypted copy of your keys on our server. " +
                 "Secure your backup with a Security Phrase.",
-            )}</p>
-            <p>{_t("For maximum security, this should be different from your account password.")}</p>
+            ) }</p>
+            <p>{ _t("For maximum security, this should be different from your account password.") }</p>
 
             <div className="mx_CreateKeyBackupDialog_primaryContainer">
                 <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@@ -268,9 +268,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
             />
 
             <details>
-                <summary>{_t("Advanced")}</summary>
+                <summary>{ _t("Advanced") }</summary>
                 <AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
-                    {_t("Set up with a Security Key")}
+                    { _t("Set up with a Security Key") }
                 </AccessibleButton>
             </details>
         </form>;
@@ -299,19 +299,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
         let passPhraseMatch = null;
         if (matchText) {
             passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
-                <div>{matchText}</div>
+                <div>{ matchText }</div>
                 <div>
                     <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
-                        {changeText}
+                        { changeText }
                     </AccessibleButton>
                 </div>
             </div>;
         }
         const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
         return <form onSubmit={this._onPassPhraseConfirmNextClick}>
-            <p>{_t(
+            <p>{ _t(
                 "Enter your Security Phrase a second time to confirm it.",
-            )}</p>
+            ) }</p>
             <div className="mx_CreateKeyBackupDialog_primaryContainer">
                 <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
                     <div>
@@ -323,7 +323,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
                             autoFocus={true}
                         />
                     </div>
-                    {passPhraseMatch}
+                    { passPhraseMatch }
                 </div>
             </div>
             <DialogButtons
@@ -337,27 +337,27 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
 
     _renderPhaseShowKey() {
         return <div>
-            <p>{_t(
+            <p>{ _t(
                 "Your Security Key is a safety net - you can use it to restore " +
                 "access to your encrypted messages if you forget your Security Phrase.",
-            )}</p>
-            <p>{_t(
+            ) }</p>
+            <p>{ _t(
                 "Keep a copy of it somewhere secure, like a password manager or even a safe.",
-            )}</p>
+            ) }</p>
             <div className="mx_CreateKeyBackupDialog_primaryContainer">
                 <div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
-                    {_t("Your Security Key")}
+                    { _t("Your Security Key") }
                 </div>
                 <div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
                     <div className="mx_CreateKeyBackupDialog_recoveryKey">
-                        <code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
+                        <code ref={this._collectRecoveryKeyNode}>{ this._keyBackupInfo.recovery_key }</code>
                     </div>
                     <div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
                         <button className="mx_Dialog_primary" onClick={this._onCopyClick}>
-                            {_t("Copy")}
+                            { _t("Copy") }
                         </button>
                         <button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
-                            {_t("Download")}
+                            { _t("Download") }
                         </button>
                     </div>
                 </div>
@@ -370,26 +370,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
         if (this.state.copied) {
             introText = _t(
                 "Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
-                {}, { b: s => <b>{s}</b> },
+                {}, { b: s => <b>{ s }</b> },
             );
         } else if (this.state.downloaded) {
             introText = _t(
                 "Your Security Key is in your <b>Downloads</b> folder.",
-                {}, { b: s => <b>{s}</b> },
+                {}, { b: s => <b>{ s }</b> },
             );
         }
         const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
         return <div>
-            {introText}
+            { introText }
             <ul>
-                <li>{_t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{s}</b> })}</li>
-                <li>{_t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{s}</b> })}</li>
-                <li>{_t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{s}</b> })}</li>
+                <li>{ _t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{ s }</b> }) }</li>
+                <li>{ _t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{ s }</b> }) }</li>
+                <li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
             </ul>
             <DialogButtons primaryButton={_t("Continue")}
                 onPrimaryButtonClick={this._createBackup}
                 hasCancel={false}>
-                <button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
+                <button onClick={this._onKeepItSafeBackClick}>{ _t("Back") }</button>
             </DialogButtons>
         </div>;
     }
@@ -404,9 +404,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
     _renderPhaseDone() {
         const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
         return <div>
-            <p>{_t(
+            <p>{ _t(
                 "Your keys are being backed up (the first backup could take a few minutes).",
-            )}</p>
+            ) }</p>
             <DialogButtons primaryButton={_t('OK')}
                 onPrimaryButtonClick={this._onDone}
                 hasCancel={false}
@@ -417,10 +417,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
     _renderPhaseOptOutConfirm() {
         const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
         return <div>
-            {_t(
+            { _t(
                 "Without setting up Secure Message Recovery, you won't be able to restore your " +
                 "encrypted message history if you log out or use another session.",
-            )}
+            ) }
             <DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
                 onPrimaryButtonClick={this._onSetUpClick}
                 hasCancel={false}
@@ -457,7 +457,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
         if (this.state.error) {
             const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
             content = <div>
-                <p>{_t("Unable to create key backup")}</p>
+                <p>{ _t("Unable to create key backup") }</p>
                 <div className="mx_Dialog_buttons">
                     <DialogButtons primaryButton={_t('Retry')}
                         onPrimaryButtonClick={this._createBackup}
@@ -499,7 +499,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
                 hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
             >
                 <div>
-                    {content}
+                    { content }
                 </div>
             </BaseDialog>
         );
diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
index e1254929db..aa78d68830 100644
--- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
@@ -475,9 +475,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
             >
                 <div className="mx_CreateSecretStorageDialog_optionTitle">
                     <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
-                    {_t("Generate a Security Key")}
+                    { _t("Generate a Security Key") }
                 </div>
-                <div>{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div>
+                <div>{ _t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
             </StyledRadioButton>
         );
     }
@@ -494,9 +494,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
             >
                 <div className="mx_CreateSecretStorageDialog_optionTitle">
                     <span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
-                    {_t("Enter a Security Phrase")}
+                    { _t("Enter a Security Phrase") }
                 </div>
-                <div>{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}</div>
+                <div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div>
             </StyledRadioButton>
         );
     }
@@ -507,13 +507,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
         const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
 
         return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
-            <p className="mx_CreateSecretStorageDialog_centeredBody">{_t(
+            <p className="mx_CreateSecretStorageDialog_centeredBody">{ _t(
                 "Safeguard against losing access to encrypted messages & data by " +
                 "backing up encryption keys on your server.",
-            )}</p>
+            ) }</p>
             <div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
-                {optionKey}
-                {optionPassphrase}
+                { optionKey }
+                { optionPassphrase }
             </div>
             <DialogButtons
                 primaryButton={_t("Continue")}
@@ -536,7 +536,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
         let nextCaption = _t("Next");
         if (this.state.canUploadKeysWithPasswordOnly) {
             authPrompt = <div>
-                <div>{_t("Enter your account password to confirm the upgrade:")}</div>
+                <div>{ _t("Enter your account password to confirm the upgrade:") }</div>
                 <div><Field
                     type="password"
                     label={_t("Password")}
@@ -548,22 +548,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
             </div>;
         } else if (!this.state.backupSigStatus.usable) {
             authPrompt = <div>
-                <div>{_t("Restore your key backup to upgrade your encryption")}</div>
+                <div>{ _t("Restore your key backup to upgrade your encryption") }</div>
             </div>;
             nextCaption = _t("Restore");
         } else {
             authPrompt = <p>
-                {_t("You'll need to authenticate with the server to confirm the upgrade.")}
+                { _t("You'll need to authenticate with the server to confirm the upgrade.") }
             </p>;
         }
 
         return <form onSubmit={this._onMigrateFormSubmit}>
-            <p>{_t(
+            <p>{ _t(
                 "Upgrade this session to allow it to verify other sessions, " +
                 "granting them access to encrypted messages and marking them " +
                 "as trusted for other users.",
-            )}</p>
-            <div>{authPrompt}</div>
+            ) }</p>
+            <div>{ authPrompt }</div>
             <DialogButtons
                 primaryButton={nextCaption}
                 onPrimaryButtonClick={this._onMigrateFormSubmit}
@@ -571,7 +571,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
                 primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
             >
                 <button type="button" className="danger" onClick={this._onCancelClick}>
-                    {_t('Skip')}
+                    { _t('Skip') }
                 </button>
             </DialogButtons>
         </form>;
@@ -579,10 +579,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 
     _renderPhasePassPhrase() {
         return <form onSubmit={this._onPassPhraseNextClick}>
-            <p>{_t(
+            <p>{ _t(
                 "Enter a security phrase only you know, as it’s used to safeguard your data. " +
                 "To be secure, you shouldn’t re-use your account password.",
-            )}</p>
+            ) }</p>
 
             <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
                 <PassphraseField
@@ -609,7 +609,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
                 <button type="button"
                     onClick={this._onCancelClick}
                     className="danger"
-                >{_t("Cancel")}</button>
+                >{ _t("Cancel") }</button>
             </DialogButtons>
         </form>;
     }
@@ -637,18 +637,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
         let passPhraseMatch = null;
         if (matchText) {
             passPhraseMatch = <div>
-                <div>{matchText}</div>
+                <div>{ matchText }</div>
                 <div>
                     <AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
-                        {changeText}
+                        { changeText }
                     </AccessibleButton>
                 </div>
             </div>;
         }
         return <form onSubmit={this._onPassPhraseConfirmNextClick}>
-            <p>{_t(
+            <p>{ _t(
                 "Enter your Security Phrase a second time to confirm it.",
-            )}</p>
+            ) }</p>
             <div className="mx_CreateSecretStorageDialog_passPhraseContainer">
                 <Field
                     type="password"
@@ -660,7 +660,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
                     autoComplete="new-password"
                 />
                 <div className="mx_CreateSecretStorageDialog_passPhraseMatch">
-                    {passPhraseMatch}
+                    { passPhraseMatch }
                 </div>
             </div>
             <DialogButtons
@@ -672,7 +672,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
                 <button type="button"
                     onClick={this._onCancelClick}
                     className="danger"
-                >{_t("Skip")}</button>
+                >{ _t("Skip") }</button>
             </DialogButtons>
         </form>;
     }
@@ -691,35 +691,35 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
             </div>;
         }
         return <div>
-            <p>{_t(
+            <p>{ _t(
                 "Store your Security Key somewhere safe, like a password manager or a safe, " +
                 "as it’s used to safeguard your encrypted data.",
-            )}</p>
+            ) }</p>
             <div className="mx_CreateSecretStorageDialog_primaryContainer">
                 <div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
                     <div className="mx_CreateSecretStorageDialog_recoveryKey">
-                        <code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
+                        <code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
                     </div>
                     <div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
                         <AccessibleButton kind='primary' className="mx_Dialog_primary"
                             onClick={this._onDownloadClick}
                             disabled={this.state.phase === PHASE_STORING}
                         >
-                            {_t("Download")}
+                            { _t("Download") }
                         </AccessibleButton>
-                        <span>{_t("or")}</span>
+                        <span>{ _t("or") }</span>
                         <AccessibleButton
                             kind='primary'
                             className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
                             onClick={this._onCopyClick}
                             disabled={this.state.phase === PHASE_STORING}
                         >
-                            {this.state.copied ? _t("Copied!") : _t("Copy")}
+                            { this.state.copied ? _t("Copied!") : _t("Copy") }
                         </AccessibleButton>
                     </div>
                 </div>
             </div>
-            {continueButton}
+            { continueButton }
         </div>;
     }
 
@@ -732,7 +732,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 
     _renderPhaseLoadError() {
         return <div>
-            <p>{_t("Unable to query secret storage status")}</p>
+            <p>{ _t("Unable to query secret storage status") }</p>
             <div className="mx_Dialog_buttons">
                 <DialogButtons primaryButton={_t('Retry')}
                     onPrimaryButtonClick={this._onLoadRetryClick}
@@ -745,17 +745,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 
     _renderPhaseSkipConfirm() {
         return <div>
-            <p>{_t(
+            <p>{ _t(
                 "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
-            )}</p>
-            <p>{_t(
+            ) }</p>
+            <p>{ _t(
                 "You can also set up Secure Backup & manage your keys in Settings.",
-            )}</p>
+            ) }</p>
             <DialogButtons primaryButton={_t('Go back')}
                 onPrimaryButtonClick={this._onGoBackClick}
                 hasCancel={false}
             >
-                <button type="button" className="danger" onClick={this._onCancel}>{_t('Cancel')}</button>
+                <button type="button" className="danger" onClick={this._onCancel}>{ _t('Cancel') }</button>
             </DialogButtons>
         </div>;
     }
@@ -787,7 +787,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
         let content;
         if (this.state.error) {
             content = <div>
-                <p>{_t("Unable to set up secret storage")}</p>
+                <p>{ _t("Unable to set up secret storage") }</p>
                 <div className="mx_Dialog_buttons">
                     <DialogButtons primaryButton={_t('Retry')}
                         onPrimaryButtonClick={this._bootstrapSecretStorage}
@@ -857,7 +857,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
                 fixedWidth={false}
             >
                 <div>
-                    {content}
+                    { content }
                 </div>
             </BaseDialog>
         );
diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js
index 4a0aa37da0..263d25c98c 100644
--- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js
+++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js
@@ -54,28 +54,28 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
         const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
 
         const title = <span className="mx_KeyBackupFailedDialog_title">
-            {_t("New Recovery Method")}
+            { _t("New Recovery Method") }
         </span>;
 
-        const newMethodDetected = <p>{_t(
+        const newMethodDetected = <p>{ _t(
             "A new Security Phrase and key for Secure Messages have been detected.",
-        )}</p>;
+        ) }</p>;
 
-        const hackWarning = <p className="warning">{_t(
+        const hackWarning = <p className="warning">{ _t(
             "If you didn't set the new recovery method, an " +
             "attacker may be trying to access your account. " +
             "Change your account password and set a new recovery " +
             "method immediately in Settings.",
-        )}</p>;
+        ) }</p>;
 
         let content;
         if (MatrixClientPeg.get().getKeyBackupEnabled()) {
             content = <div>
-                {newMethodDetected}
-                <p>{_t(
+                { newMethodDetected }
+                <p>{ _t(
                     "This session is encrypting history using the new recovery method.",
-                )}</p>
-                {hackWarning}
+                ) }</p>
+                { hackWarning }
                 <DialogButtons
                     primaryButton={_t("OK")}
                     onPrimaryButtonClick={this.onOkClick}
@@ -85,8 +85,8 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
             </div>;
         } else {
             content = <div>
-                {newMethodDetected}
-                {hackWarning}
+                { newMethodDetected }
+                { hackWarning }
                 <DialogButtons
                     primaryButton={_t("Set up Secure Messages")}
                     onPrimaryButtonClick={this.onSetupClick}
@@ -101,7 +101,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
                 onFinished={this.props.onFinished}
                 title={title}
             >
-                {content}
+                { content }
             </BaseDialog>
         );
     }
diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js
index f0f8a5273b..f586c9430a 100644
--- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js
+++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js
@@ -46,7 +46,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
         const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
 
         const title = <span className="mx_KeyBackupFailedDialog_title">
-            {_t("Recovery Method Removed")}
+            { _t("Recovery Method Removed") }
         </span>;
 
         return (
@@ -55,21 +55,21 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
                 title={title}
             >
                 <div>
-                    <p>{_t(
+                    <p>{ _t(
                         "This session has detected that your Security Phrase and key " +
                         "for Secure Messages have been removed.",
-                    )}</p>
-                    <p>{_t(
+                    ) }</p>
+                    <p>{ _t(
                         "If you did this accidentally, you can setup Secure Messages on " +
                         "this session which will re-encrypt this session's message " +
                         "history with a new recovery method.",
-                    )}</p>
-                    <p className="warning">{_t(
+                    ) }</p>
+                    <p className="warning">{ _t(
                         "If you didn't remove the recovery method, an " +
                         "attacker may be trying to access your account. " +
                         "Change your account password and set a new recovery " +
                         "method immediately in Settings.",
-                    )}</p>
+                    ) }</p>
                     <DialogButtons
                         primaryButton={_t("Set up Secure Messages")}
                         onPrimaryButtonClick={this.onSetupClick}
diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx
index 2fc77e9a17..d3175edbdb 100644
--- a/src/autocomplete/EmojiProvider.tsx
+++ b/src/autocomplete/EmojiProvider.tsx
@@ -25,7 +25,6 @@ import { PillCompletion } from './Components';
 import { ICompletion, ISelectionRange } from './Autocompleter';
 import { uniq, sortBy } from 'lodash';
 import SettingsStore from "../settings/SettingsStore";
-import { shortcodeToUnicode } from '../HtmlUtils';
 import { EMOJI, IEmoji } from '../emoji';
 
 import EMOTICON_REGEX from 'emojibase-regex/emoticon';
@@ -36,20 +35,18 @@ const LIMIT = 20;
 // anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs
 const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
 
-interface IEmojiShort {
+interface ISortedEmoji {
     emoji: IEmoji;
-    shortname: string;
     _orderBy: number;
 }
 
-const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => {
+const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => {
     if (a.group === b.group) {
         return a.order - b.order;
     }
     return a.group - b.group;
 }).map((emoji, index) => ({
     emoji,
-    shortname: `:${emoji.shortcodes[0]}:`,
     // Include the index so that we can preserve the original order
     _orderBy: index,
 }));
@@ -64,20 +61,18 @@ function score(query, space) {
 }
 
 export default class EmojiProvider extends AutocompleteProvider {
-    matcher: QueryMatcher<IEmojiShort>;
-    nameMatcher: QueryMatcher<IEmojiShort>;
+    matcher: QueryMatcher<ISortedEmoji>;
+    nameMatcher: QueryMatcher<ISortedEmoji>;
 
     constructor() {
         super(EMOJI_REGEX);
-        this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
-            keys: ['emoji.emoticon', 'shortname'],
-            funcs: [
-                (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
-            ],
+        this.matcher = new QueryMatcher<ISortedEmoji>(SORTED_EMOJI, {
+            keys: ['emoji.emoticon'],
+            funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)],
             // For matching against ascii equivalents
             shouldMatchWordsOnly: false,
         });
-        this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
+        this.nameMatcher = new QueryMatcher(SORTED_EMOJI, {
             keys: ['emoji.annotation'],
             // For removing punctuation
             shouldMatchWordsOnly: true,
@@ -105,34 +100,33 @@ export default class EmojiProvider extends AutocompleteProvider {
 
             const sorters = [];
             // make sure that emoticons come first
-            sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
+            sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
 
-            // then sort by score (Infinity if matchedString not in shortname)
-            sorters.push((c) => score(matchedString, c.shortname));
+            // then sort by score (Infinity if matchedString not in shortcode)
+            sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
             // then sort by max score of all shortcodes, trim off the `:`
-            sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s))));
-            // If the matchedString is not empty, sort by length of shortname. Example:
+            sorters.push(c => Math.min(
+                ...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
+            ));
+            // If the matchedString is not empty, sort by length of shortcode. Example:
             //  matchedString = ":bookmark"
             //  completions = [":bookmark:", ":bookmark_tabs:", ...]
             if (matchedString.length > 1) {
-                sorters.push((c) => c.shortname.length);
+                sorters.push(c => c.emoji.shortcodes[0].length);
             }
             // Finally, sort by original ordering
-            sorters.push((c) => c._orderBy);
+            sorters.push(c => c._orderBy);
             completions = sortBy(uniq(completions), sorters);
 
-            completions = completions.map(({ shortname }) => {
-                const unicode = shortcodeToUnicode(shortname);
-                return {
-                    completion: unicode,
-                    component: (
-                        <PillCompletion title={shortname} aria-label={unicode}>
-                            <span>{ unicode }</span>
-                        </PillCompletion>
-                    ),
-                    range,
-                };
-            }).slice(0, LIMIT);
+            completions = completions.map(c => ({
+                completion: c.emoji.unicode,
+                component: (
+                    <PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
+                        <span>{ c.emoji.unicode }</span>
+                    </PillCompletion>
+                ),
+                range,
+            })).slice(0, LIMIT);
         }
         return completions;
     }
diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx
index d8f17c54d0..182743abb3 100644
--- a/src/autocomplete/UserProvider.tsx
+++ b/src/autocomplete/UserProvider.tsx
@@ -109,7 +109,7 @@ export default class UserProvider extends AutocompleteProvider {
         limit = -1,
     ): Promise<ICompletion[]> {
         // lazy-load user list into matcher
-        if (!this.users) this._makeUsers();
+        if (!this.users) this.makeUsers();
 
         let completions = [];
         const { command, range } = this.getCurrentCommand(rawQuery, selection, force);
@@ -147,7 +147,7 @@ export default class UserProvider extends AutocompleteProvider {
         return _t('Users');
     }
 
-    _makeUsers() {
+    private makeUsers() {
         const events = this.room.getLiveTimeline().getEvents();
         const lastSpoken = {};
 
diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts
new file mode 100644
index 0000000000..384f20cd4e
--- /dev/null
+++ b/src/components/structures/CallEventGrouper.ts
@@ -0,0 +1,145 @@
+/*
+Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+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 { EventType } from "matrix-js-sdk/src/@types/event";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
+import CallHandler, { CallHandlerEvent } from '../../CallHandler';
+import { EventEmitter } from 'events';
+import { MatrixClientPeg } from "../../MatrixClientPeg";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+
+export enum CallEventGrouperEvent {
+    StateChanged = "state_changed",
+    SilencedChanged = "silenced_changed",
+}
+
+const SUPPORTED_STATES = [
+    CallState.Connected,
+    CallState.Connecting,
+    CallState.Ringing,
+];
+
+export enum CustomCallState {
+    Missed = "missed",
+}
+
+export default class CallEventGrouper extends EventEmitter {
+    private events: Set<MatrixEvent> = new Set<MatrixEvent>();
+    private call: MatrixCall;
+    public state: CallState | CustomCallState;
+
+    constructor() {
+        super();
+
+        CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall);
+        CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
+    }
+
+    private get invite(): MatrixEvent {
+        return [...this.events].find((event) => event.getType() === EventType.CallInvite);
+    }
+
+    private get hangup(): MatrixEvent {
+        return [...this.events].find((event) => event.getType() === EventType.CallHangup);
+    }
+
+    private get reject(): MatrixEvent {
+        return [...this.events].find((event) => event.getType() === EventType.CallReject);
+    }
+
+    public get isVoice(): boolean {
+        const invite = this.invite;
+        if (!invite) return;
+
+        // FIXME: Find a better way to determine this from the event?
+        if (invite.getContent()?.offer?.sdp?.indexOf('m=video') !== -1) return false;
+        return true;
+    }
+
+    public get hangupReason(): string | null {
+        return this.hangup?.getContent()?.reason;
+    }
+
+    /**
+     * Returns true if there are only events from the other side - we missed the call
+     */
+    private get callWasMissed(): boolean {
+        return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId());
+    }
+
+    private get callId(): string {
+        return [...this.events][0].getContent().call_id;
+    }
+
+    private onSilencedCallsChanged = () => {
+        const newState = CallHandler.sharedInstance().isCallSilenced(this.callId);
+        this.emit(CallEventGrouperEvent.SilencedChanged, newState);
+    };
+
+    public answerCall = () => {
+        this.call?.answer();
+    };
+
+    public rejectCall = () => {
+        this.call?.reject();
+    };
+
+    public callBack = () => {
+        defaultDispatcher.dispatch({
+            action: 'place_call',
+            type: this.isVoice ? CallType.Voice : CallType.Video,
+            room_id: [...this.events][0]?.getRoomId(),
+        });
+    };
+
+    public toggleSilenced = () => {
+        const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId);
+        silenced ?
+            CallHandler.sharedInstance().unSilenceCall(this.callId) :
+            CallHandler.sharedInstance().silenceCall(this.callId);
+    };
+
+    private setCallListeners() {
+        if (!this.call) return;
+        this.call.addListener(CallEvent.State, this.setState);
+    }
+
+    private setState = () => {
+        if (SUPPORTED_STATES.includes(this.call?.state)) {
+            this.state = this.call.state;
+        } else {
+            if (this.callWasMissed) this.state = CustomCallState.Missed;
+            else if (this.reject) this.state = CallState.Ended;
+            else if (this.hangup) this.state = CallState.Ended;
+            else if (this.invite && this.call) this.state = CallState.Connecting;
+        }
+        this.emit(CallEventGrouperEvent.StateChanged, this.state);
+    };
+
+    private setCall = () => {
+        if (this.call) return;
+
+        this.call = CallHandler.sharedInstance().getCallById(this.callId);
+        this.setCallListeners();
+        this.setState();
+    };
+
+    public add(event: MatrixEvent) {
+        this.events.add(event);
+        this.setCall();
+    }
+}
diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js
index 037d7c251c..5e31048207 100644
--- a/src/components/structures/CustomRoomTagPanel.js
+++ b/src/components/structures/CustomRoomTagPanel.js
@@ -56,7 +56,7 @@ class CustomRoomTagPanel extends React.Component {
         return (<div className={classes}>
             <div className="mx_CustomRoomTagPanel_divider" />
             <AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
-                {tags}
+                { tags }
             </AutoHideScrollbar>
         </div>);
     }
@@ -84,7 +84,7 @@ class CustomRoomTagTile extends React.Component {
                 "mx_TagTile_badge": true,
                 "mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
             });
-            badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badgeNotifState.count)}</div>);
+            badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badgeNotifState.count) }</div>);
         }
 
         return (
diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js
index 628c16f322..472a43e142 100644
--- a/src/components/structures/EmbeddedPage.js
+++ b/src/components/structures/EmbeddedPage.js
@@ -125,11 +125,11 @@ export default class EmbeddedPage extends React.PureComponent {
 
         if (this.props.scrollbar) {
             return <AutoHideScrollbar className={classes}>
-                {content}
+                { content }
             </AutoHideScrollbar>;
         } else {
             return <div className={classes}>
-                {content}
+                { content }
             </div>;
         }
     }
diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx
index 36f774a130..c6d72d04bb 100644
--- a/src/components/structures/FilePanel.tsx
+++ b/src/components/structures/FilePanel.tsx
@@ -241,8 +241,8 @@ class FilePanel extends React.Component<IProps, IState> {
         // wrap a TimelinePanel with the jump-to-event bits turned off.
 
         const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
-            <h2>{_t('No files visible in this room')}</h2>
-            <p>{_t('Attach files from chat or just drag and drop them anywhere in a room.')}</p>
+            <h2>{ _t('No files visible in this room') }</h2>
+            <p>{ _t('Attach files from chat or just drag and drop them anywhere in a room.') }</p>
         </div>);
 
         const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
@@ -262,7 +262,7 @@ class FilePanel extends React.Component<IProps, IState> {
                         manageReadReceipts={false}
                         manageReadMarkers={false}
                         timelineSet={this.state.timelineSet}
-                        showUrlPreview = {false}
+                        showUrlPreview={false}
                         onPaginationRequest={this.onPaginationRequest}
                         tileShape={TileShape.FileGrid}
                         resizeNotifier={this.props.resizeNotifier}
diff --git a/src/components/structures/GenericErrorPage.js b/src/components/structures/GenericErrorPage.js
index c9ed4ae622..017d365273 100644
--- a/src/components/structures/GenericErrorPage.js
+++ b/src/components/structures/GenericErrorPage.js
@@ -28,8 +28,8 @@ export default class GenericErrorPage extends React.PureComponent {
     render() {
         return <div className='mx_GenericErrorPage'>
             <div className='mx_GenericErrorPage_box'>
-                <h1>{this.props.title}</h1>
-                <p>{this.props.message}</p>
+                <h1>{ this.props.title }</h1>
+                <p>{ this.props.message }</p>
             </div>
         </div>;
     }
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index f31f302b29..9d69fce801 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -819,12 +819,12 @@ export default class GroupView extends React.Component {
         let hostingSignup = null;
         if (hostingSignupLink && this.state.isUserPrivileged) {
             hostingSignup = <div className="mx_GroupView_hostingSignup">
-                {_t(
+                { _t(
                     "Want more than a community? <a>Get your own server</a>", {},
                     {
-                        a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
+                        a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{ sub }</a>,
                     },
-                )}
+                ) }
                 <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
                     <img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
                 </a>
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index 3d5e386b00..3bd2c68c6c 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -429,7 +429,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
                     onSelectRoom={this.selectRoom}
                 />
 
-                {dialPadButton}
+                { dialPadButton }
 
                 <AccessibleTooltipButton
                     className={classNames("mx_LeftPanel_exploreButton", {
@@ -448,7 +448,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
             leftLeftPanel = (
                 <div className="mx_LeftPanel_GroupFilterPanelContainer">
                     <GroupFilterPanel />
-                    {SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
+                    { SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
                 </div>
             );
         }
@@ -476,11 +476,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
 
         return (
             <div className={containerClasses} ref={this.ref}>
-                {leftLeftPanel}
+                { leftLeftPanel }
                 <aside className="mx_LeftPanel_roomListContainer">
-                    {this.renderHeader()}
-                    {this.renderSearchDialExplore()}
-                    {this.renderBreadcrumbs()}
+                    { this.renderHeader() }
+                    { this.renderSearchDialExplore() }
+                    { this.renderBreadcrumbs() }
                     <RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
                     <div className="mx_LeftPanel_roomListWrapper">
                         <div
@@ -490,7 +490,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
                             // overflow:scroll;, so force it out of tab order.
                             tabIndex={-1}
                         >
-                            {roomList}
+                            { roomList }
                         </div>
                     </div>
                     { !this.props.isMinimized && <LeftPanelWidget /> }
diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx
index e0b597b883..144c0e3051 100644
--- a/src/components/structures/LeftPanelWidget.tsx
+++ b/src/components/structures/LeftPanelWidget.tsx
@@ -125,15 +125,15 @@ const LeftPanelWidget: React.FC = () => {
                     <span>{ WidgetUtils.getWidgetName(app) }</span>
                 </AccessibleButton>
 
-                {/* Code for the maximise button for once we have full screen widgets */}
-                {/*<AccessibleTooltipButton
+                { /* Code for the maximise button for once we have full screen widgets */ }
+                { /*<AccessibleTooltipButton
                     tabIndex={tabIndex}
                     onClick={() => {
                     }}
                     className="mx_LeftPanelWidget_maximizeButton"
                     tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
                     title={_t("Maximize")}
-                />*/}
+                />*/ }
             </div>
         </div>
 
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 6c086ed17c..d496c4ad21 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -17,8 +17,8 @@ limitations under the License.
 */
 
 import * as React from 'react';
-import * as PropTypes from 'prop-types';
 import { MatrixClient } from 'matrix-js-sdk/src/client';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
 
 import { Key } from '../../Keyboard';
 import PageTypes from '../../PageTypes';
@@ -79,6 +79,8 @@ function canElementReceiveInput(el) {
 
 interface IProps {
     matrixClient: MatrixClient;
+    // Called with the credentials of a registered user (if they were a ROU that
+    // transitioned to PWLU)
     onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
     hideToSRUsers: boolean;
     resizeNotifier: ResizeNotifier;
@@ -140,18 +142,6 @@ interface IState {
 class LoggedInView extends React.Component<IProps, IState> {
     static displayName = 'LoggedInView';
 
-    static propTypes = {
-        matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
-        page_type: PropTypes.string.isRequired,
-        onRoomCreated: PropTypes.func,
-
-        // Called with the credentials of a registered user (if they were a ROU that
-        // transitioned to PWLU)
-        onRegistered: PropTypes.func,
-
-        // and lots and lots of other stuff.
-    };
-
     protected readonly _matrixClient: MatrixClient;
     protected readonly _roomView: React.RefObject<any>;
     protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
@@ -181,10 +171,10 @@ class LoggedInView extends React.Component<IProps, IState> {
     }
 
     componentDidMount() {
-        document.addEventListener('keydown', this._onNativeKeyDown, false);
+        document.addEventListener('keydown', this.onNativeKeyDown, false);
         CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
 
-        this._updateServerNoticeEvents();
+        this.updateServerNoticeEvents();
 
         this._matrixClient.on("accountData", this.onAccountData);
         this._matrixClient.on("sync", this.onSync);
@@ -200,13 +190,13 @@ class LoggedInView extends React.Component<IProps, IState> {
             "useCompactLayout", null, this.onCompactLayoutChanged,
         );
 
-        this.resizer = this._createResizer();
+        this.resizer = this.createResizer();
         this.resizer.attach();
-        this._loadResizerPreferences();
+        this.loadResizerPreferences();
     }
 
     componentWillUnmount() {
-        document.removeEventListener('keydown', this._onNativeKeyDown, false);
+        document.removeEventListener('keydown', this.onNativeKeyDown, false);
         CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
         this._matrixClient.removeListener("accountData", this.onAccountData);
         this._matrixClient.removeListener("sync", this.onSync);
@@ -221,37 +211,37 @@ class LoggedInView extends React.Component<IProps, IState> {
         });
     };
 
-    canResetTimelineInRoom = (roomId) => {
+    public canResetTimelineInRoom = (roomId: string) => {
         if (!this._roomView.current) {
             return true;
         }
         return this._roomView.current.canResetTimeline();
     };
 
-    _createResizer() {
-        let size;
-        let collapsed;
+    private createResizer() {
+        let panelSize;
+        let panelCollapsed;
         const collapseConfig: ICollapseConfig = {
             // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
             toggleSize: 206 - 50,
-            onCollapsed: (_collapsed) => {
-                collapsed = _collapsed;
-                if (_collapsed) {
+            onCollapsed: (collapsed) => {
+                panelCollapsed = collapsed;
+                if (collapsed) {
                     dis.dispatch({ action: "hide_left_panel" });
                     window.localStorage.setItem("mx_lhs_size", '0');
                 } else {
                     dis.dispatch({ action: "show_left_panel" });
                 }
             },
-            onResized: (_size) => {
-                size = _size;
+            onResized: (size) => {
+                panelSize = size;
                 this.props.resizeNotifier.notifyLeftHandleResized();
             },
             onResizeStart: () => {
                 this.props.resizeNotifier.startResizing();
             },
             onResizeStop: () => {
-                if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
+                if (!panelCollapsed) window.localStorage.setItem("mx_lhs_size", '' + panelSize);
                 this.props.resizeNotifier.stopResizing();
             },
             isItemCollapsed: domNode => {
@@ -267,7 +257,7 @@ class LoggedInView extends React.Component<IProps, IState> {
         return resizer;
     }
 
-    _loadResizerPreferences() {
+    private loadResizerPreferences() {
         let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
         if (isNaN(lhsSize)) {
             lhsSize = 350;
@@ -275,7 +265,7 @@ class LoggedInView extends React.Component<IProps, IState> {
         this.resizer.forHandleAt(0).resize(lhsSize);
     }
 
-    onAccountData = (event) => {
+    private onAccountData = (event: MatrixEvent) => {
         if (event.getType() === "m.ignored_user_list") {
             dis.dispatch({ action: "ignore_state_changed" });
         }
@@ -307,16 +297,16 @@ class LoggedInView extends React.Component<IProps, IState> {
         }
 
         if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
-            this._updateServerNoticeEvents();
+            this.updateServerNoticeEvents();
         } else {
-            this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
+            this.calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
         }
     };
 
     onRoomStateEvents = (ev, state) => {
         const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
         if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
-            this._updateServerNoticeEvents();
+            this.updateServerNoticeEvents();
         }
     };
 
@@ -326,7 +316,7 @@ class LoggedInView extends React.Component<IProps, IState> {
         });
     };
 
-    _calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
+    private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
         const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
         if (error) {
             usageLimitEventContent = syncError.error.data;
@@ -346,7 +336,7 @@ class LoggedInView extends React.Component<IProps, IState> {
         }
     }
 
-    _updateServerNoticeEvents = async () => {
+    private updateServerNoticeEvents = async () => {
         const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
         if (!serverNoticeList) return [];
 
@@ -378,7 +368,7 @@ class LoggedInView extends React.Component<IProps, IState> {
             );
         });
         const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
-        this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
+        this.calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
         this.setState({
             usageLimitEventContent,
             usageLimitEventTs: pinnedEventTs,
@@ -387,7 +377,7 @@ class LoggedInView extends React.Component<IProps, IState> {
         });
     };
 
-    _onPaste = (ev) => {
+    private onPaste = (ev) => {
         let canReceiveInput = false;
         let element = ev.target;
         // test for all parents because the target can be a child of a contenteditable element
@@ -425,22 +415,22 @@ class LoggedInView extends React.Component<IProps, IState> {
     We also listen with a native listener on the document to get keydown events when no element is focused.
     Bubbling is irrelevant here as the target is the body element.
     */
-    _onReactKeyDown = (ev) => {
+    private onReactKeyDown = (ev) => {
         // events caught while bubbling up on the root element
         // of this component, so something must be focused.
-        this._onKeyDown(ev);
+        this.onKeyDown(ev);
     };
 
-    _onNativeKeyDown = (ev) => {
+    private onNativeKeyDown = (ev) => {
         // only pass this if there is no focused element.
-        // if there is, _onKeyDown will be called by the
+        // if there is, onKeyDown will be called by the
         // react keydown handler that respects the react bubbling order.
         if (ev.target === document.body) {
-            this._onKeyDown(ev);
+            this.onKeyDown(ev);
         }
     };
 
-    _onKeyDown = (ev) => {
+    private onKeyDown = (ev) => {
         let handled = false;
 
         const roomAction = getKeyBindingsManager().getRoomAction(ev);
@@ -450,7 +440,7 @@ class LoggedInView extends React.Component<IProps, IState> {
             case RoomAction.JumpToFirstMessage:
             case RoomAction.JumpToLatestMessage:
                 // pass the event down to the scroll panel
-                this._onScrollKeyPressed(ev);
+                this.onScrollKeyPressed(ev);
                 handled = true;
                 break;
             case RoomAction.FocusSearch:
@@ -565,7 +555,7 @@ class LoggedInView extends React.Component<IProps, IState> {
      * dispatch a page-up/page-down/etc to the appropriate component
      * @param {Object} ev The key event
      */
-    _onScrollKeyPressed = (ev) => {
+    private onScrollKeyPressed = (ev) => {
         if (this._roomView.current) {
             this._roomView.current.handleScrollKey(ev);
         }
@@ -625,8 +615,8 @@ class LoggedInView extends React.Component<IProps, IState> {
         return (
             <MatrixClientContext.Provider value={this._matrixClient}>
                 <div
-                    onPaste={this._onPaste}
-                    onKeyDown={this._onReactKeyDown}
+                    onPaste={this.onPaste}
+                    onKeyDown={this.onReactKeyDown}
                     className='mx_MatrixChat_wrapper'
                     aria-hidden={this.props.hideToSRUsers}
                 >
@@ -644,7 +634,7 @@ class LoggedInView extends React.Component<IProps, IState> {
                 <CallContainer />
                 <NonUrgentToastContainer />
                 <HostSignupContainer />
-                {audioFeedArraysForCalls}
+                { audioFeedArraysForCalls }
             </MatrixClientContext.Provider>
         );
     }
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 785838ffca..8cfe35c4cf 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -431,7 +431,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     UNSAFE_componentWillUpdate(props, state) {
         if (this.shouldTrackPageChange(this.state, state)) {
             this.startPageChangeTimer();
@@ -1112,7 +1112,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         if (memberCount === 1) {
             warnings.push((
                 <span className="warning" key="only_member_warning">
-                    {' '/* Whitespace, otherwise the sentences get smashed together */ }
+                    { ' '/* Whitespace, otherwise the sentences get smashed together */ }
                     { _t("You are the only person here. " +
                         "If you leave, no one will be able to join in the future, including you.") }
                 </span>
@@ -1127,7 +1127,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
             if (rule !== "public") {
                 warnings.push((
                     <span className="warning" key="non_public_warning">
-                        {' '/* Whitespace, otherwise the sentences get smashed together */ }
+                        { ' '/* Whitespace, otherwise the sentences get smashed together */ }
                         { isSpace
                             ? _t("This space is not public. You will not be able to rejoin without an invite.")
                             : _t("This room is not public. You will not be able to rejoin without an invite.") }
@@ -1155,7 +1155,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                         : _t(
                             "Are you sure you want to leave the room '%(roomName)s'?",
                             { roomName: roomToLeave.name },
-                        )}
+                        ) }
                     { warnings }
                 </span>
             ),
@@ -1864,13 +1864,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         dis.dispatch({ action: 'timeline_resize' });
     }
 
-    onRoomCreated(roomId: string) {
-        dis.dispatch({
-            action: "view_room",
-            room_id: roomId,
-        });
-    }
-
     onRegisterClick = () => {
         this.showScreen("register");
     };
@@ -2043,7 +2036,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                         {...this.state}
                         ref={this.loggedInView}
                         matrixClient={MatrixClientPeg.get()}
-                        onRoomCreated={this.onRoomCreated}
                         onRegistered={this.onRegistered}
                         currentRoomId={this.state.currentRoomId}
                     />
@@ -2053,15 +2045,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                 let errorBox;
                 if (this.state.syncError && !isStoreError) {
                     errorBox = <div className="mx_MatrixChat_syncError">
-                        {messageForSyncError(this.state.syncError)}
+                        { messageForSyncError(this.state.syncError) }
                     </div>;
                 }
                 view = (
                     <div className="mx_MatrixChat_splash">
-                        {errorBox}
+                        { errorBox }
                         <Spinner />
                         <a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
-                            {_t('Logout')}
+                            { _t('Logout') }
                         </a>
                     </div>
                 );
@@ -2124,7 +2116,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         }
 
         return <ErrorBoundary>
-            {view}
+            { view }
         </ErrorBoundary>;
     }
 }
diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 8977549697..514cf4db09 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -36,6 +36,7 @@ import DMRoomMap from "../../utils/DMRoomMap";
 import NewRoomIntro from "../views/rooms/NewRoomIntro";
 import { replaceableComponent } from "../../utils/replaceableComponent";
 import defaultDispatcher from '../../dispatcher/dispatcher';
+import CallEventGrouper from "./CallEventGrouper";
 import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
 import ScrollPanel, { IScrollState } from "./ScrollPanel";
 import EventListSummary from '../views/elements/EventListSummary';
@@ -232,6 +233,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
     private readonly showTypingNotificationsWatcherRef: string;
     private eventNodes: Record<string, HTMLElement>;
 
+    // A map of <callId, CallEventGrouper>
+    private callEventGroupers = new Map<string, CallEventGrouper>();
+
     constructor(props, context) {
         super(props, context);
 
@@ -576,6 +580,20 @@ export default class MessagePanel extends React.Component<IProps, IState> {
             const last = (mxEv === lastShownEvent);
             const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
 
+            if (
+                mxEv.getType().indexOf("m.call.") === 0 ||
+                mxEv.getType().indexOf("org.matrix.call.") === 0
+            ) {
+                const callId = mxEv.getContent().call_id;
+                if (this.callEventGroupers.has(callId)) {
+                    this.callEventGroupers.get(callId).add(mxEv);
+                } else {
+                    const callEventGrouper = new CallEventGrouper();
+                    callEventGrouper.add(mxEv);
+                    this.callEventGroupers.set(callId, callEventGrouper);
+                }
+            }
+
             if (grouper) {
                 if (grouper.shouldGroup(mxEv)) {
                     grouper.add(mxEv, this.showHiddenEvents);
@@ -653,8 +671,10 @@ export default class MessagePanel extends React.Component<IProps, IState> {
         }
 
         let willWantDateSeparator = false;
+        let lastInSection = true;
         if (nextEvent) {
             willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
+            lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEvent.getSender();
         }
 
         // is this a continuation of the previous message?
@@ -690,6 +710,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
         // it's successful: we received it.
         isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
 
+        const callEventGrouper = this.callEventGroupers.get(mxEv.getContent().call_id);
+
         // use txnId as key if available so that we don't remount during sending
         ret.push(
             <TileErrorBoundary key={mxEv.getTxnId() || eventId} mxEvent={mxEv}>
@@ -712,7 +734,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
                     isTwelveHour={this.props.isTwelveHour}
                     permalinkCreator={this.props.permalinkCreator}
                     last={last}
-                    lastInSection={willWantDateSeparator}
+                    lastInSection={lastInSection}
                     lastSuccessful={isLastSuccessful}
                     isSelectedEvent={highlight}
                     getRelationsForEvent={this.props.getRelationsForEvent}
@@ -720,6 +742,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
                     layout={this.props.layout}
                     enableFlair={this.props.enableFlair}
                     showReadReceipts={this.props.showReadReceipts}
+                    callEventGrouper={callEventGrouper}
+                    hideSender={this.props.room.getMembers().length <= 2 && this.props.layout === Layout.Bubble}
                 />
             </TileErrorBoundary>,
         );
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index 87447b6aba..fca5613ede 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -121,7 +121,7 @@ export default class MyGroups extends React.Component {
                         ) }
                     </div>
                 </div>
-                {/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
+                { /*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
                     <AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
                         <img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
                     </AccessibleButton>
@@ -137,7 +137,7 @@ export default class MyGroups extends React.Component {
                             { 'i': (sub) => <i>{ sub }</i> })
                         }
                     </div>
-                </div>*/}
+                </div>*/ }
             </div>
             <BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
             <div className="mx_MyGroups_content">
diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx
index a2d419b4ba..6e914c40fb 100644
--- a/src/components/structures/NonUrgentToastContainer.tsx
+++ b/src/components/structures/NonUrgentToastContainer.tsx
@@ -51,14 +51,14 @@ export default class NonUrgentToastContainer extends React.PureComponent<IProps,
         const toasts = this.state.toasts.map((t, i) => {
             return (
                 <div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}>
-                    {React.createElement(t, {})}
+                    { React.createElement(t, {}) }
                 </div>
             );
         });
 
         return (
             <div className="mx_NonUrgentToastContainer" role="alert">
-                {toasts}
+                { toasts }
             </div>
         );
     }
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx
index 8c8fab7ece..8abc161bab 100644
--- a/src/components/structures/NotificationPanel.tsx
+++ b/src/components/structures/NotificationPanel.tsx
@@ -35,8 +35,8 @@ interface IProps {
 export default class NotificationPanel extends React.PureComponent<IProps> {
     render() {
         const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
-            <h2>{_t('You’re all caught up')}</h2>
-            <p>{_t('You have no visible notifications.')}</p>
+            <h2>{ _t('You’re all caught up') }</h2>
+            <p>{ _t('You have no visible notifications.') }</p>
         </div>);
 
         let content;
diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx
index 2a3448b017..95d70e913a 100644
--- a/src/components/structures/RightPanel.tsx
+++ b/src/components/structures/RightPanel.tsx
@@ -17,6 +17,7 @@ limitations under the License.
 
 import React from 'react';
 import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomState } from "matrix-js-sdk/src/models/room-state";
 import { User } from "matrix-js-sdk/src/models/user";
 import { RoomMember } from "matrix-js-sdk/src/models/room-member";
 import { MatrixEvent } from "matrix-js-sdk/src/models/event";
@@ -152,7 +153,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
+    UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line
         if (newProps.groupId !== this.props.groupId) {
             this.unregisterGroupStore();
             this.initGroupStore(newProps.groupId);
@@ -174,7 +175,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
         });
     };
 
-    private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
+    private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember) => {
         if (!this.props.room || member.roomId !== this.props.room.roomId) {
             return;
         }
diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx
index aa5baaf8c2..84e8de8221 100644
--- a/src/components/structures/RoomDirectory.tsx
+++ b/src/components/structures/RoomDirectory.tsx
@@ -589,7 +589,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
         // We use onMouseDown instead of onClick, so that we can avoid text getting selected
         return [
             <div
-                key={ `${room.room_id}_avatar` }
+                key={`${room.room_id}_avatar`}
                 onMouseDown={(ev) => this.onRoomClicked(room, ev)}
                 className="mx_RoomDirectory_roomAvatar"
             >
@@ -603,7 +603,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
                 />
             </div>,
             <div
-                key={ `${room.room_id}_description` }
+                key={`${room.room_id}_description`}
                 onMouseDown={(ev) => this.onRoomClicked(room, ev)}
                 className="mx_RoomDirectory_roomDescription"
             >
@@ -626,14 +626,14 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
                 </div>
             </div>,
             <div
-                key={ `${room.room_id}_memberCount` }
+                key={`${room.room_id}_memberCount`}
                 onMouseDown={(ev) => this.onRoomClicked(room, ev)}
                 className="mx_RoomDirectory_roomMemberCount"
             >
                 { room.num_joined_members }
             </div>,
             <div
-                key={ `${room.room_id}_preview` }
+                key={`${room.room_id}_preview`}
                 onMouseDown={(ev) => this.onRoomClicked(room, ev)}
                 // cancel onMouseDown otherwise shift-clicking highlights text
                 className="mx_RoomDirectory_preview"
@@ -641,7 +641,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
                 { previewButton }
             </div>,
             <div
-                key={ `${room.room_id}_join` }
+                key={`${room.room_id}_join`}
                 onMouseDown={(ev) => this.onRoomClicked(room, ev)}
                 className="mx_RoomDirectory_join"
             >
@@ -796,7 +796,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
                     showJoinButton={showJoinButton}
                     initialText={this.props.initialText}
                 />
-                {dropdown}
+                { dropdown }
             </div>;
         }
         const explanation =
@@ -814,16 +814,16 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
             }) : _t("Explore rooms");
         return (
             <BaseDialog
-                className={'mx_RoomDirectory_dialog'}
+                className="mx_RoomDirectory_dialog"
                 hasCancel={true}
                 onFinished={this.onFinished}
                 title={title}
             >
                 <div className="mx_RoomDirectory">
-                    {explanation}
+                    { explanation }
                     <div className="mx_RoomDirectory_list">
-                        {listHeader}
-                        {content}
+                        { listHeader }
+                        { content }
                     </div>
                 </div>
             </BaseDialog>
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index e8080b4f7b..9acfb7bb8e 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -209,9 +209,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
 
         return (
             <div className={classes}>
-                {icon}
-                {input}
-                {clearButton}
+                { icon }
+                { input }
+                { clearButton }
             </div>
         );
     }
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index 80ea26c3f2..ac4d197346 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -222,17 +222,17 @@ export default class RoomStatusBar extends React.PureComponent {
 
         let buttonRow = <>
             <AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
-                {_t("Delete all")}
+                { _t("Delete all") }
             </AccessibleButton>
             <AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
-                {_t("Retry all")}
+                { _t("Retry all") }
             </AccessibleButton>
         </>;
         if (this.state.isResending) {
             buttonRow = <>
                 <InlineSpinner w={20} h={20} />
-                {/* span for css */}
-                <span>{_t("Sending")}</span>
+                { /* span for css */ }
+                <span>{ _t("Sending") }</span>
             </>;
         }
 
@@ -253,7 +253,7 @@ export default class RoomStatusBar extends React.PureComponent {
                         </div>
                     </div>
                     <div className="mx_RoomStatusBar_unsentButtonBar">
-                        {buttonRow}
+                        { buttonRow }
                     </div>
                 </div>
             </div>
@@ -270,10 +270,10 @@ export default class RoomStatusBar extends React.PureComponent {
                                 height="24" title="/!\ " alt="/!\ " />
                             <div>
                                 <div className="mx_RoomStatusBar_connectionLostBar_title">
-                                    {_t('Connectivity to the server has been lost.')}
+                                    { _t('Connectivity to the server has been lost.') }
                                 </div>
                                 <div className="mx_RoomStatusBar_connectionLostBar_desc">
-                                    {_t('Sent messages will be stored until your connection has returned.')}
+                                    { _t('Sent messages will be stored until your connection has returned.') }
                                 </div>
                             </div>
                         </div>
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 0c10a2aeca..7860e65362 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1892,10 +1892,10 @@ export default class RoomView extends React.Component<IProps, IState> {
                     className="mx_RoomView_auxPanel_hiddenHighlights"
                     onClick={this.onHiddenHighlightsClick}
                 >
-                    {_t(
+                    { _t(
                         "You have %(count)s unread notifications in a prior version of this room.",
                         { count: hiddenHighlightCount },
-                    )}
+                    ) }
                 </AccessibleButton>
             );
         }
@@ -2007,7 +2007,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                 onScroll={this.onMessageListScroll}
                 onUserScroll={this.onUserScroll}
                 onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
-                showUrlPreview = {this.state.showUrlPreview}
+                showUrlPreview={this.state.showUrlPreview}
                 className={messagePanelClassNames}
                 membersLoaded={this.state.membersLoaded}
                 permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
@@ -2057,7 +2057,7 @@ export default class RoomView extends React.Component<IProps, IState> {
         return (
             <RoomContext.Provider value={this.state}>
                 <main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
-                    {showChatEffects && this.roomView.current &&
+                    { showChatEffects && this.roomView.current &&
                         <EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
                     }
                     <ErrorBoundary>
@@ -2076,22 +2076,22 @@ export default class RoomView extends React.Component<IProps, IState> {
                         />
                         <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
                             <div className="mx_RoomView_body">
-                                {auxPanel}
+                                { auxPanel }
                                 <div className={timelineClasses}>
-                                    {fileDropTarget}
-                                    {topUnreadMessagesBar}
-                                    {jumpToBottom}
-                                    {messagePanel}
-                                    {searchResultsPanel}
+                                    { fileDropTarget }
+                                    { topUnreadMessagesBar }
+                                    { jumpToBottom }
+                                    { messagePanel }
+                                    { searchResultsPanel }
                                 </div>
                                 <div className={statusBarAreaClass}>
                                     <div className="mx_RoomView_statusAreaBox">
                                         <div className="mx_RoomView_statusAreaBox_line" />
-                                        {statusBar}
+                                        { statusBar }
                                     </div>
                                 </div>
-                                {previewBar}
-                                {messageComposer}
+                                { previewBar }
+                                { messageComposer }
                             </div>
                         </MainSplit>
                     </ErrorBoundary>
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js
index 5c966d2d3a..3cf4b9b593 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.js
@@ -136,7 +136,7 @@ export default class SearchBox extends React.Component {
                 key="button"
                 tabIndex={-1}
                 className="mx_SearchBox_closeButton"
-                onClick={ () => {this._clearSearch("button"); } }>
+                onClick={() => {this._clearSearch("button"); }}>
             </AccessibleButton>) : undefined;
 
         // show a shorter placeholder when blurred, if requested
@@ -153,12 +153,12 @@ export default class SearchBox extends React.Component {
                     type="text"
                     ref={this._search}
                     className={"mx_textinput_icon mx_textinput_search " + className}
-                    value={ this.state.searchTerm }
-                    onFocus={ this._onFocus }
-                    onChange={ this.onChange }
-                    onKeyDown={ this._onKeyDown }
+                    value={this.state.searchTerm}
+                    onFocus={this._onFocus}
+                    onChange={this.onChange}
+                    onKeyDown={this._onKeyDown}
                     onBlur={this._onBlur}
-                    placeholder={ placeholder }
+                    placeholder={placeholder}
                     autoComplete="off"
                     autoFocus={this.props.autoFocus}
                 />
diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index 27539a5c3c..038c1df514 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -404,7 +404,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
     const [saving, setSaving] = useState(false);
 
     if (summaryError) {
-        return <p>{_t("Your server does not support showing space hierarchies.")}</p>;
+        return <p>{ _t("Your server does not support showing space hierarchies.") }</p>;
     }
 
     let content;
@@ -569,7 +569,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
     return <>
         <SearchBox
             className="mx_textinput_icon mx_textinput_search"
-            placeholder={ _t("Search names and descriptions") }
+            placeholder={_t("Search names and descriptions")}
             onSearch={setQuery}
             autoFocus={true}
             initialValue={initialText}
@@ -608,7 +608,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, onFinished, initialText }
                 { _t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
                     null,
                     { a: sub => {
-                        return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
+                        return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{ sub }</AccessibleButton>;
                     } },
                 ) }
 
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 0ee68a9578..06b2f4a629 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -146,7 +146,7 @@ const SpaceInfo = ({ space }) => {
     return <div className="mx_SpaceRoomView_info">
         { visibilitySection }
         { joinRule === "public" && <RoomMemberCount room={space}>
-            {(count) => count > 0 ? (
+            { (count) => count > 0 ? (
                 <AccessibleButton
                     kind="link"
                     onClick={() => {
@@ -159,7 +159,7 @@ const SpaceInfo = ({ space }) => {
                 >
                     { _t("%(count)s members", { count }) }
                 </AccessibleButton>
-            ) : null}
+            ) : null }
         </RoomMemberCount> }
     </div>;
 };
@@ -292,7 +292,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
         </h1>
         <SpaceInfo space={space} />
         <RoomTopic room={space}>
-            {(topic, ref) =>
+            { (topic, ref) =>
                 <div className="mx_SpaceRoomView_preview_topic" ref={ref}>
                     { topic }
                 </div>
@@ -419,12 +419,12 @@ const SpaceLanding = ({ space }) => {
         <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
         <div className="mx_SpaceRoomView_landing_name">
             <RoomName room={space}>
-                {(name) => {
+                { (name) => {
                     const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
                         <h1>{ name }</h1>
                     </div> };
                     return _t("Welcome to <name/>", {}, tags) as JSX.Element;
-                }}
+                } }
             </RoomName>
         </div>
         <div className="mx_SpaceRoomView_landing_info">
@@ -434,11 +434,11 @@ const SpaceLanding = ({ space }) => {
             { settingsButton }
         </div>
         <RoomTopic room={space}>
-            {(topic, ref) => (
+            { (topic, ref) => (
                 <div className="mx_SpaceRoomView_landing_topic" ref={ref}>
                     { topic }
                 </div>
-            )}
+            ) }
         </RoomTopic>
         <SpaceFeedbackPrompt />
         <hr />
@@ -458,7 +458,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
     const numFields = 3;
     const placeholders = [_t("General"), _t("Random"), _t("Support")];
     const [roomNames, setRoomName] = useStateArray(numFields, [_t("General"), _t("Random"), ""]);
-    const fields = new Array(numFields).fill(0).map((_, i) => {
+    const fields = new Array(numFields).fill(0).map((x, i) => {
         const name = "roomName" + i;
         return <Field
             key={name}
@@ -625,7 +625,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
     const numFields = 3;
     const fieldRefs: RefObject<Field>[] = [useRef(), useRef(), useRef()];
     const [emailAddresses, setEmailAddress] = useStateArray(numFields, "");
-    const fields = new Array(numFields).fill(0).map((_, i) => {
+    const fields = new Array(numFields).fill(0).map((x, i) => {
         const name = "emailAddress" + i;
         return <Field
             key={name}
diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx
index 19694cd769..037c33a600 100644
--- a/src/components/structures/TabbedView.tsx
+++ b/src/components/structures/TabbedView.tsx
@@ -74,7 +74,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
         tabLocation: TabLocation.LEFT,
     };
 
-    private _getActiveTabIndex() {
+    private getActiveTabIndex() {
         if (!this.state || !this.state.activeTabIndex) return 0;
         return this.state.activeTabIndex;
     }
@@ -84,7 +84,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
      * @param {Tab} tab the tab to show
      * @private
      */
-    private _setActiveTab(tab: Tab) {
+    private setActiveTab(tab: Tab) {
         const idx = this.props.tabs.indexOf(tab);
         if (idx !== -1) {
             if (this.props.onChange) this.props.onChange(tab.id);
@@ -94,23 +94,23 @@ export default class TabbedView extends React.Component<IProps, IState> {
         }
     }
 
-    private _renderTabLabel(tab: Tab) {
+    private renderTabLabel(tab: Tab) {
         let classes = "mx_TabbedView_tabLabel ";
 
         const idx = this.props.tabs.indexOf(tab);
-        if (idx === this._getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active";
+        if (idx === this.getActiveTabIndex()) classes += "mx_TabbedView_tabLabel_active";
 
         let tabIcon = null;
         if (tab.icon) {
             tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
         }
 
-        const onClickHandler = () => this._setActiveTab(tab);
+        const onClickHandler = () => this.setActiveTab(tab);
 
         const label = _t(tab.label);
         return (
             <AccessibleButton className={classes} key={"tab_label_" + tab.label} onClick={onClickHandler}>
-                {tabIcon}
+                { tabIcon }
                 <span className="mx_TabbedView_tabLabel_text">
                     { label }
                 </span>
@@ -118,19 +118,19 @@ export default class TabbedView extends React.Component<IProps, IState> {
         );
     }
 
-    private _renderTabPanel(tab: Tab): React.ReactNode {
+    private renderTabPanel(tab: Tab): React.ReactNode {
         return (
             <div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
                 <AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
-                    {tab.body}
+                    { tab.body }
                 </AutoHideScrollbar>
             </div>
         );
     }
 
     public render(): React.ReactNode {
-        const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
-        const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
+        const labels = this.props.tabs.map(tab => this.renderTabLabel(tab));
+        const panel = this.renderTabPanel(this.props.tabs[this.getActiveTabIndex()]);
 
         const tabbedViewClasses = classNames({
             'mx_TabbedView': true,
@@ -141,9 +141,9 @@ export default class TabbedView extends React.Component<IProps, IState> {
         return (
             <div className={tabbedViewClasses}>
                 <div className="mx_TabbedView_tabLabels">
-                    {labels}
+                    { labels }
                 </div>
-                {panel}
+                { panel }
             </div>
         );
     }
diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx
index 59930bf41d..c4210c68a8 100644
--- a/src/components/structures/TimelinePanel.tsx
+++ b/src/components/structures/TimelinePanel.tsx
@@ -277,7 +277,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
     }
 
     // TODO: [REACT-WARNING] Move into constructor
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     UNSAFE_componentWillMount() {
         if (this.props.manageReadReceipts) {
             this.updateReadReceiptOnUserActivity();
@@ -290,7 +290,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     UNSAFE_componentWillReceiveProps(newProps) {
         if (newProps.timelineSet !== this.props.timelineSet) {
             // throw new Error("changing timelineSet on a TimelinePanel is not supported");
@@ -1448,7 +1448,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
         if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
             return (
                 <div className={this.props.className + " mx_RoomView_messageListWrapper"}>
-                    <div className="mx_RoomView_empty">{this.props.empty}</div>
+                    <div className="mx_RoomView_empty">{ this.props.empty }</div>
                 </div>
             );
         }
diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx
index 79a73735f4..b7b0b7c652 100644
--- a/src/components/structures/ToastContainer.tsx
+++ b/src/components/structures/ToastContainer.tsx
@@ -37,14 +37,14 @@ export default class ToastContainer extends React.Component<{}, IState> {
         // toasts may dismiss themselves in their didMount if they find
         // they're already irrelevant by the time they're mounted, and
         // our own componentDidMount is too late.
-        ToastStore.sharedInstance().on('update', this._onToastStoreUpdate);
+        ToastStore.sharedInstance().on('update', this.onToastStoreUpdate);
     }
 
     componentWillUnmount() {
-        ToastStore.sharedInstance().removeListener('update', this._onToastStoreUpdate);
+        ToastStore.sharedInstance().removeListener('update', this.onToastStoreUpdate);
     }
 
-    _onToastStoreUpdate = () => {
+    private onToastStoreUpdate = () => {
         this.setState({
             toasts: ToastStore.sharedInstance().getToasts(),
             countSeen: ToastStore.sharedInstance().getCountSeen(),
@@ -75,10 +75,10 @@ export default class ToastContainer extends React.Component<{}, IState> {
             });
             toast = (<div className={toastClasses}>
                 <div className="mx_Toast_title">
-                    <h2>{title}</h2>
-                    <span>{countIndicator}</span>
+                    <h2>{ title }</h2>
+                    <span>{ countIndicator }</span>
                 </div>
-                <div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
+                <div className="mx_Toast_body">{ React.createElement(component, toastProps) }</div>
             </div>);
 
             containerClasses = classNames("mx_ToastContainer", {
@@ -88,7 +88,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
         return toast
             ? (
                 <div className={containerClasses} role="alert">
-                    {toast}
+                    { toast }
                 </div>
             )
             : null;
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index c8e90a1c0a..6ee53da5d1 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -104,7 +104,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
         const uploadSize = filesize(this.state.currentUpload.total);
         return (
             <div className="mx_UploadBar">
-                <div className="mx_UploadBar_filename">{uploadText} ({uploadSize})</div>
+                <div className="mx_UploadBar_filename">{ uploadText } ({ uploadSize })</div>
                 <AccessibleButton onClick={this.onCancelClick} className='mx_UploadBar_cancel' />
                 <ProgressBar value={this.state.currentUpload.loaded} max={this.state.currentUpload.total} />
             </div>
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 34575ba582..0a30367e4b 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -342,20 +342,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
         if (MatrixClientPeg.get().isGuest()) {
             topSection = (
                 <div className="mx_UserMenu_contextMenu_header mx_UserMenu_contextMenu_guestPrompts">
-                    {_t("Got an account? <a>Sign in</a>", {}, {
+                    { _t("Got an account? <a>Sign in</a>", {}, {
                         a: sub => (
                             <AccessibleButton kind="link" onClick={this.onSignInClick}>
-                                {sub}
+                                { sub }
                             </AccessibleButton>
                         ),
-                    })}
-                    {_t("New here? <a>Create an account</a>", {}, {
+                    }) }
+                    { _t("New here? <a>Create an account</a>", {}, {
                         a: sub => (
                             <AccessibleButton kind="link" onClick={this.onRegisterClick}>
-                                {sub}
+                                { sub }
                             </AccessibleButton>
                         ),
-                    })}
+                    }) }
                 </div>
             );
         } else if (hostSignupConfig) {
@@ -394,17 +394,17 @@ export default class UserMenu extends React.Component<IProps, IState> {
         let primaryHeader = (
             <div className="mx_UserMenu_contextMenu_name">
                 <span className="mx_UserMenu_contextMenu_displayName">
-                    {OwnProfileStore.instance.displayName}
+                    { OwnProfileStore.instance.displayName }
                 </span>
                 <span className="mx_UserMenu_contextMenu_userId">
-                    {MatrixClientPeg.get().getUserId()}
+                    { MatrixClientPeg.get().getUserId() }
                 </span>
             </div>
         );
         let primaryOptionList = (
             <React.Fragment>
                 <IconizedContextMenuOptionList>
-                    {homeButton}
+                    { homeButton }
                     <IconizedContextMenuOption
                         iconClassName="mx_UserMenu_iconBell"
                         label={_t("Notification settings")}
@@ -420,11 +420,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
                         label={_t("All settings")}
                         onClick={(e) => this.onSettingsOpen(e, null)}
                     />
-                    {/* <IconizedContextMenuOption
+                    { /* <IconizedContextMenuOption
                         iconClassName="mx_UserMenu_iconArchive"
                         label={_t("Archived rooms")}
                         onClick={this.onShowArchived}
-                    /> */}
+                    /> */ }
                     { feedbackButton }
                 </IconizedContextMenuOptionList>
                 <IconizedContextMenuOptionList red>
@@ -443,7 +443,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
             primaryHeader = (
                 <div className="mx_UserMenu_contextMenu_name">
                     <span className="mx_UserMenu_contextMenu_displayName">
-                        {prototypeCommunityName}
+                        { prototypeCommunityName }
                     </span>
                 </div>
             );
@@ -470,13 +470,13 @@ export default class UserMenu extends React.Component<IProps, IState> {
             }
             primaryOptionList = (
                 <IconizedContextMenuOptionList>
-                    {settingsOption}
+                    { settingsOption }
                     <IconizedContextMenuOption
                         iconClassName="mx_UserMenu_iconMembers"
                         label={_t("Members")}
                         onClick={this.onCommunityMembersClick}
                     />
-                    {inviteOption}
+                    { inviteOption }
                 </IconizedContextMenuOptionList>
             );
             secondarySection = (
@@ -485,10 +485,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
                     <div className="mx_UserMenu_contextMenu_header">
                         <div className="mx_UserMenu_contextMenu_name">
                             <span className="mx_UserMenu_contextMenu_displayName">
-                                {OwnProfileStore.instance.displayName}
+                                { OwnProfileStore.instance.displayName }
                             </span>
                             <span className="mx_UserMenu_contextMenu_userId">
-                                {MatrixClientPeg.get().getUserId()}
+                                { MatrixClientPeg.get().getUserId() }
                             </span>
                         </div>
                     </div>
@@ -540,7 +540,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
             className={classes}
         >
             <div className="mx_UserMenu_contextMenu_header">
-                {primaryHeader}
+                { primaryHeader }
                 <AccessibleTooltipButton
                     className="mx_UserMenu_contextMenu_themeButton"
                     onClick={this.onSwitchThemeClick}
@@ -553,9 +553,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
                     />
                 </AccessibleTooltipButton>
             </div>
-            {topSection}
-            {primaryOptionList}
-            {secondarySection}
+            { topSection }
+            { primaryOptionList }
+            { secondarySection }
         </IconizedContextMenu>;
     };
 
@@ -570,27 +570,27 @@ export default class UserMenu extends React.Component<IProps, IState> {
 
         let isPrototype = false;
         let menuName = _t("User menu");
-        let name = <span className="mx_UserMenu_userName">{displayName}</span>;
+        let name = <span className="mx_UserMenu_userName">{ displayName }</span>;
         let buttons = (
             <span className="mx_UserMenu_headerButtons">
-                {/* masked image in CSS */}
+                { /* masked image in CSS */ }
             </span>
         );
         let dnd;
         if (this.state.selectedSpace) {
             name = (
                 <div className="mx_UserMenu_doubleName">
-                    <span className="mx_UserMenu_userName">{displayName}</span>
+                    <span className="mx_UserMenu_userName">{ displayName }</span>
                     <RoomName room={this.state.selectedSpace}>
-                        {(roomName) => <span className="mx_UserMenu_subUserName">{roomName}</span>}
+                        { (roomName) => <span className="mx_UserMenu_subUserName">{ roomName }</span> }
                     </RoomName>
                 </div>
             );
         } else if (prototypeCommunityName) {
             name = (
                 <div className="mx_UserMenu_doubleName">
-                    <span className="mx_UserMenu_userName">{prototypeCommunityName}</span>
-                    <span className="mx_UserMenu_subUserName">{displayName}</span>
+                    <span className="mx_UserMenu_userName">{ prototypeCommunityName }</span>
+                    <span className="mx_UserMenu_subUserName">{ displayName }</span>
                 </div>
             );
             menuName = _t("Community and user menu");
@@ -598,8 +598,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
         } else if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
             name = (
                 <div className="mx_UserMenu_doubleName">
-                    <span className="mx_UserMenu_userName">{_t("Home")}</span>
-                    <span className="mx_UserMenu_subUserName">{displayName}</span>
+                    <span className="mx_UserMenu_userName">{ _t("Home") }</span>
+                    <span className="mx_UserMenu_subUserName">{ displayName }</span>
                 </div>
             );
             isPrototype = true;
@@ -647,20 +647,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
                                 className="mx_UserMenu_userAvatar"
                             />
                         </span>
-                        {name}
-                        {this.state.pendingRoomJoin.size > 0 && (
+                        { name }
+                        { this.state.pendingRoomJoin.size > 0 && (
                             <InlineSpinner>
                                 <TooltipButton helpText={_t(
                                     "Currently joining %(count)s rooms",
                                     { count: this.state.pendingRoomJoin.size },
                                 )} />
                             </InlineSpinner>
-                        )}
-                        {dnd}
-                        {buttons}
+                        ) }
+                        { dnd }
+                        { buttons }
                     </div>
                 </ContextMenuButton>
-                {this.renderContextMenu()}
+                { this.renderContextMenu() }
             </React.Fragment>
         );
     }
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index b69a92dd61..2bfa20e892 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -63,23 +63,23 @@ export default class ViewSource extends React.Component {
                 <>
                     <details open className="mx_ViewSource_details">
                         <summary>
-                            <span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
+                            <span className="mx_ViewSource_heading">{ _t("Decrypted event source") }</span>
                         </summary>
-                        <SyntaxHighlight className="json">{JSON.stringify(decryptedEventSource, null, 2)}</SyntaxHighlight>
+                        <SyntaxHighlight className="json">{ JSON.stringify(decryptedEventSource, null, 2) }</SyntaxHighlight>
                     </details>
                     <details className="mx_ViewSource_details">
                         <summary>
-                            <span className="mx_ViewSource_heading">{_t("Original event source")}</span>
+                            <span className="mx_ViewSource_heading">{ _t("Original event source") }</span>
                         </summary>
-                        <SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
+                        <SyntaxHighlight className="json">{ JSON.stringify(originalEventSource, null, 2) }</SyntaxHighlight>
                     </details>
                 </>
             );
         } else {
             return (
                 <>
-                    <div className="mx_ViewSource_heading">{_t("Original event source")}</div>
-                    <SyntaxHighlight className="json">{JSON.stringify(originalEventSource, null, 2)}</SyntaxHighlight>
+                    <div className="mx_ViewSource_heading">{ _t("Original event source") }</div>
+                    <SyntaxHighlight className="json">{ JSON.stringify(originalEventSource, null, 2) }</SyntaxHighlight>
                 </>
             );
         }
@@ -110,7 +110,7 @@ export default class ViewSource extends React.Component {
         if (isStateEvent) {
             return (
                 <MatrixClientContext.Consumer>
-                    {(cli) => (
+                    { (cli) => (
                         <SendCustomEvent
                             room={cli.getRoom(roomId)}
                             forceStateEvent={true}
@@ -121,7 +121,7 @@ export default class ViewSource extends React.Component {
                                 stateKey: mxEvent.getStateKey(),
                             }}
                         />
-                    )}
+                    ) }
                 </MatrixClientContext.Consumer>
             );
         } else {
@@ -142,7 +142,7 @@ export default class ViewSource extends React.Component {
             };
             return (
                 <MatrixClientContext.Consumer>
-                    {(cli) => (
+                    { (cli) => (
                         <SendCustomEvent
                             room={cli.getRoom(roomId)}
                             forceStateEvent={false}
@@ -153,7 +153,7 @@ export default class ViewSource extends React.Component {
                                 evContent: JSON.stringify(newContent, null, "\t"),
                             }}
                         />
-                    )}
+                    ) }
                 </MatrixClientContext.Consumer>
             );
         }
@@ -176,16 +176,16 @@ export default class ViewSource extends React.Component {
         return (
             <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
                 <div>
-                    <div>Room ID: {roomId}</div>
-                    <div>Event ID: {eventId}</div>
+                    <div>Room ID: { roomId }</div>
+                    <div>Event ID: { eventId }</div>
                     <div className="mx_ViewSource_separator" />
-                    {isEditing ? this.editSourceContent() : this.viewSourceContent()}
+                    { isEditing ? this.editSourceContent() : this.viewSourceContent() }
                 </div>
-                {!isEditing && canEdit && (
+                { !isEditing && canEdit && (
                     <div className="mx_Dialog_buttons">
-                        <button onClick={() => this.onEdit()}>{_t("Edit")}</button>
+                        <button onClick={() => this.onEdit()}>{ _t("Edit") }</button>
                     </div>
-                )}
+                ) }
             </BaseDialog>
         );
     }
diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx
index 2f37e60450..8c3d5e80a0 100644
--- a/src/components/structures/auth/CompleteSecurity.tsx
+++ b/src/components/structures/auth/CompleteSecurity.tsx
@@ -79,8 +79,8 @@ export default class CompleteSecurity extends React.Component<IProps, IState> {
             <AuthPage>
                 <CompleteSecurityBody>
                     <h2 className="mx_CompleteSecurity_header">
-                        {icon}
-                        {title}
+                        { icon }
+                        { title }
                     </h2>
                     <div className="mx_CompleteSecurity_body">
                         <SetupEncryptionBody onFinished={this.props.onFinished} />
diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx
index 6382e143f9..3755505f3d 100644
--- a/src/components/structures/auth/ForgotPassword.tsx
+++ b/src/components/structures/auth/ForgotPassword.tsx
@@ -101,7 +101,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
         if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
             newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
@@ -239,14 +239,14 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
             });
             serverDeadSection = (
                 <div className={classes}>
-                    {this.state.serverDeadError}
+                    { this.state.serverDeadError }
                 </div>
             );
         }
 
         return <div>
-            {errorText}
-            {serverDeadSection}
+            { errorText }
+            { serverDeadSection }
             <ServerPicker
                 serverConfig={this.props.serverConfig}
                 onServerConfigChange={this.props.onServerConfigChange}
@@ -289,10 +289,10 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
                         autoComplete="new-password"
                     />
                 </div>
-                <span>{_t(
+                <span>{ _t(
                     'A verification email will be sent to your inbox to confirm ' +
                     'setting your new password.',
-                )}</span>
+                ) }</span>
                 <input
                     className="mx_Login_submit"
                     type="submit"
@@ -300,7 +300,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
                 />
             </form>
             <a className="mx_AuthBody_changeFlow" onClick={this.onLoginClick} href="#">
-                {_t('Sign in instead')}
+                { _t('Sign in instead') }
             </a>
         </div>;
     }
@@ -312,8 +312,8 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
 
     renderEmailSent() {
         return <div>
-            {_t("An email has been sent to %(emailAddress)s. Once you've followed the " +
-                "link it contains, click below.", { emailAddress: this.state.email })}
+            { _t("An email has been sent to %(emailAddress)s. Once you've followed the " +
+                "link it contains, click below.", { emailAddress: this.state.email }) }
             <br />
             <input className="mx_Login_submit" type="button" onClick={this.onVerify}
                 value={_t('I have verified my email address')} />
@@ -322,12 +322,12 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
 
     renderDone() {
         return <div>
-            <p>{_t("Your password has been reset.")}</p>
-            <p>{_t(
+            <p>{ _t("Your password has been reset.") }</p>
+            <p>{ _t(
                 "You have been logged out of all sessions and will no longer receive " +
                 "push notifications. To re-enable notifications, sign in again on each " +
                 "device.",
-            )}</p>
+            ) }</p>
             <input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
                 value={_t('Return to login screen')} />
         </div>;
@@ -358,7 +358,7 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
                 <AuthHeader />
                 <AuthBody>
                     <h2> { _t('Set a new password') } </h2>
-                    {resetPasswordJsx}
+                    { resetPasswordJsx }
                 </AuthBody>
             </AuthPage>
         );
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 9f12521a34..6a3d339681 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -144,7 +144,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     UNSAFE_componentWillMount() {
         this.initLoginLogic(this.props.serverConfig);
     }
@@ -154,7 +154,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     UNSAFE_componentWillReceiveProps(newProps) {
         if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
             newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
@@ -239,8 +239,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
                 );
                 errorText = (
                     <div>
-                        <div>{errorTop}</div>
-                        <div className="mx_Login_smallError">{errorDetail}</div>
+                        <div>{ errorTop }</div>
+                        <div className="mx_Login_smallError">{ errorDetail }</div>
                     </div>
                 );
             } else if (error.httpStatus === 401 || error.httpStatus === 403) {
@@ -251,10 +251,10 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
                         <div>
                             <div>{ _t('Incorrect username and/or password.') }</div>
                             <div className="mx_Login_smallError">
-                                {_t(
+                                { _t(
                                     'Please note you are logging into the %(hs)s server, not matrix.org.',
                                     { hs: this.props.serverConfig.hsName },
-                                )}
+                                ) }
                             </div>
                         </div>
                     );
@@ -565,7 +565,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
             });
             serverDeadSection = (
                 <div className={classes}>
-                    {this.state.serverDeadError}
+                    { this.state.serverDeadError }
                 </div>
             );
         }
@@ -578,15 +578,15 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
                     { this.props.isSyncing ? _t("Syncing...") : _t("Signing In...") }
                 </div>
                 { this.props.isSyncing && <div className="mx_AuthBody_paddedFooter_subtitle">
-                    {_t("If you've joined lots of rooms, this might take a while")}
+                    { _t("If you've joined lots of rooms, this might take a while") }
                 </div> }
             </div>;
         } else if (SettingsStore.getValue(UIFeature.Registration)) {
             footer = (
                 <span className="mx_AuthBody_changeFlow">
-                    {_t("New? <a>Create account</a>", {}, {
+                    { _t("New? <a>Create account</a>", {}, {
                         a: sub => <a onClick={this.onTryRegisterClick} href="#">{ sub }</a>,
-                    })}
+                    }) }
                 </span>
             );
         }
@@ -596,8 +596,8 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
                 <AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
                 <AuthBody>
                     <h2>
-                        {_t('Sign in')}
-                        {loader}
+                        { _t('Sign in') }
+                        { loader }
                     </h2>
                     { errorTextSection }
                     { serverDeadSection }
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 8d32981e57..549e47260f 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -141,7 +141,7 @@ export default class Registration extends React.Component<IProps, IState> {
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     UNSAFE_componentWillReceiveProps(newProps) {
         if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
             newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
@@ -290,8 +290,8 @@ export default class Registration extends React.Component<IProps, IState> {
                     },
                 );
                 msg = <div>
-                    <p>{errorTop}</p>
-                    <p>{errorDetail}</p>
+                    <p>{ errorTop }</p>
+                    <p>{ errorDetail }</p>
                 </div>;
             } else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
                 let msisdnAvailable = false;
@@ -482,13 +482,13 @@ export default class Registration extends React.Component<IProps, IState> {
                         fragmentAfterLogin={this.props.fragmentAfterLogin}
                     />
                     <h3 className="mx_AuthBody_centered">
-                        {_t(
+                        { _t(
                             "%(ssoButtons)s Or %(usernamePassword)s",
                             {
                                 ssoButtons: "",
                                 usernamePassword: "",
                             },
-                        ).trim()}
+                        ).trim() }
                     </h3>
                 </React.Fragment>;
             }
@@ -526,15 +526,15 @@ export default class Registration extends React.Component<IProps, IState> {
             });
             serverDeadSection = (
                 <div className={classes}>
-                    {this.state.serverDeadError}
+                    { this.state.serverDeadError }
                 </div>
             );
         }
 
         const signIn = <span className="mx_AuthBody_changeFlow">
-            {_t("Already have an account? <a>Sign in here</a>", {}, {
+            { _t("Already have an account? <a>Sign in here</a>", {}, {
                 a: sub => <a onClick={this.onLoginClick} href="#">{ sub }</a>,
-            })}
+            }) }
         </span>;
 
         // Only show the 'go back' button if you're not looking at the form
@@ -550,43 +550,43 @@ export default class Registration extends React.Component<IProps, IState> {
             let regDoneText;
             if (this.state.differentLoggedInUserId) {
                 regDoneText = <div>
-                    <p>{_t(
+                    <p>{ _t(
                         "Your new account (%(newAccountId)s) is registered, but you're already " +
                         "logged into a different account (%(loggedInUserId)s).", {
                             newAccountId: this.state.registeredUsername,
                             loggedInUserId: this.state.differentLoggedInUserId,
                         },
-                    )}</p>
+                    ) }</p>
                     <p><AccessibleButton element="span" className="mx_linkButton" onClick={async event => {
                         const sessionLoaded = await this.onLoginClickWithCheck(event);
                         if (sessionLoaded) {
                             dis.dispatch({ action: "view_welcome_page" });
                         }
                     }}>
-                        {_t("Continue with previous account")}
+                        { _t("Continue with previous account") }
                     </AccessibleButton></p>
                 </div>;
             } else if (this.state.formVals.password) {
                 // We're the client that started the registration
-                regDoneText = <h3>{_t(
+                regDoneText = <h3>{ _t(
                     "<a>Log in</a> to your new account.", {},
                     {
-                        a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
+                        a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{ sub }</a>,
                     },
-                )}</h3>;
+                ) }</h3>;
             } else {
                 // We're not the original client: the user probably got to us by clicking the
                 // email validation link. We can't offer a 'go straight to your account' link
                 // as we don't have the original creds.
-                regDoneText = <h3>{_t(
+                regDoneText = <h3>{ _t(
                     "You can now close this window or <a>log in</a> to your new account.", {},
                     {
-                        a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
+                        a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{ sub }</a>,
                     },
-                )}</h3>;
+                ) }</h3>;
             }
             body = <div>
-                <h2>{_t("Registration Successful")}</h2>
+                <h2>{ _t("Registration Successful") }</h2>
                 { regDoneText }
             </div>;
         } else {
diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx
index c7ce74077b..6731156807 100644
--- a/src/components/structures/auth/SetupEncryptionBody.tsx
+++ b/src/components/structures/auth/SetupEncryptionBody.tsx
@@ -152,7 +152,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
             let useRecoveryKeyButton;
             if (recoveryKeyPrompt) {
                 useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this.onUsePassphraseClick}>
-                    {recoveryKeyPrompt}
+                    { recoveryKeyPrompt }
                 </AccessibleButton>;
             }
 
@@ -165,15 +165,15 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
 
             return (
                 <div>
-                    <p>{_t(
+                    <p>{ _t(
                         "Verify your identity to access encrypted messages and prove your identity to others.",
-                    )}</p>
+                    ) }</p>
 
                     <div className="mx_CompleteSecurity_actionRow">
-                        {verifyButton}
-                        {useRecoveryKeyButton}
+                        { verifyButton }
+                        { useRecoveryKeyButton }
                         <AccessibleButton kind="danger" onClick={this.onSkipClick}>
-                            {_t("Skip")}
+                            { _t("Skip") }
                         </AccessibleButton>
                     </div>
                 </div>
@@ -181,25 +181,25 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
         } else if (phase === Phase.Done) {
             let message;
             if (this.state.backupInfo) {
-                message = <p>{_t(
+                message = <p>{ _t(
                     "Your new session is now verified. It has access to your " +
                     "encrypted messages, and other users will see it as trusted.",
-                )}</p>;
+                ) }</p>;
             } else {
-                message = <p>{_t(
+                message = <p>{ _t(
                     "Your new session is now verified. Other users will see it as trusted.",
-                )}</p>;
+                ) }</p>;
             }
             return (
                 <div>
                     <div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified" />
-                    {message}
+                    { message }
                     <div className="mx_CompleteSecurity_actionRow">
                         <AccessibleButton
                             kind="primary"
                             onClick={this.onDoneClick}
                         >
-                            {_t("Done")}
+                            { _t("Done") }
                         </AccessibleButton>
                     </div>
                 </div>
@@ -207,23 +207,23 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
         } else if (phase === Phase.ConfirmSkip) {
             return (
                 <div>
-                    <p>{_t(
+                    <p>{ _t(
                         "Without verifying, you won’t have access to all your messages " +
                         "and may appear as untrusted to others.",
-                    )}</p>
+                    ) }</p>
                     <div className="mx_CompleteSecurity_actionRow">
                         <AccessibleButton
                             className="warning"
                             kind="secondary"
                             onClick={this.onSkipConfirmClick}
                         >
-                            {_t("Skip")}
+                            { _t("Skip") }
                         </AccessibleButton>
                         <AccessibleButton
                             kind="danger"
                             onClick={this.onSkipBackClick}
                         >
-                            {_t("Go Back")}
+                            { _t("Go Back") }
                         </AccessibleButton>
                     </div>
                 </div>
diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx
index d232f55dd1..fffec949fe 100644
--- a/src/components/structures/auth/SoftLogout.tsx
+++ b/src/components/structures/auth/SoftLogout.tsx
@@ -219,7 +219,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
         if (this.state.loginView === LOGIN_VIEW.PASSWORD) {
             let error = null;
             if (this.state.errorText) {
-                error = <span className='mx_Login_error'>{this.state.errorText}</span>;
+                error = <span className='mx_Login_error'>{ this.state.errorText }</span>;
             }
 
             if (!introText) {
@@ -228,8 +228,8 @@ export default class SoftLogout extends React.Component<IProps, IState> {
 
             return (
                 <form onSubmit={this.onPasswordLogin}>
-                    <p>{introText}</p>
-                    {error}
+                    <p>{ introText }</p>
+                    { error }
                     <Field
                         type="password"
                         label={_t("Password")}
@@ -243,10 +243,10 @@ export default class SoftLogout extends React.Component<IProps, IState> {
                         type="submit"
                         disabled={this.state.busy}
                     >
-                        {_t("Sign In")}
+                        { _t("Sign In") }
                     </AccessibleButton>
                     <AccessibleButton onClick={this.onForgotPassword} kind="link">
-                        {_t("Forgotten your password?")}
+                        { _t("Forgotten your password?") }
                     </AccessibleButton>
                 </form>
             );
@@ -262,7 +262,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
 
             return (
                 <div>
-                    <p>{introText}</p>
+                    <p>{ introText }</p>
                     <SSOButtons
                         matrixClient={MatrixClientPeg.get()}
                         flow={flow}
@@ -277,10 +277,10 @@ export default class SoftLogout extends React.Component<IProps, IState> {
         // Default: assume unsupported/error
         return (
             <p>
-                {_t(
+                { _t(
                     "You cannot sign in to your account. Please contact your " +
                     "homeserver admin for more information.",
-                )}
+                ) }
             </p>
         );
     }
@@ -291,25 +291,25 @@ export default class SoftLogout extends React.Component<IProps, IState> {
                 <AuthHeader />
                 <AuthBody>
                     <h2>
-                        {_t("You're signed out")}
+                        { _t("You're signed out") }
                     </h2>
 
-                    <h3>{_t("Sign in")}</h3>
+                    <h3>{ _t("Sign in") }</h3>
                     <div>
-                        {this.renderSignInSection()}
+                        { this.renderSignInSection() }
                     </div>
 
-                    <h3>{_t("Clear personal data")}</h3>
+                    <h3>{ _t("Clear personal data") }</h3>
                     <p>
-                        {_t(
+                        { _t(
                             "Warning: Your personal data (including encryption keys) is still stored " +
                             "in this session. Clear it if you're finished using this session, or want to sign " +
                             "in to another account.",
-                        )}
+                        ) }
                     </p>
                     <div>
                         <AccessibleButton onClick={this.onClearAll} kind="danger">
-                            {_t("Clear all data")}
+                            { _t("Clear all data") }
                         </AccessibleButton>
                     </div>
                 </AuthBody>
diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx
index 66efa64658..748b1c9ffc 100644
--- a/src/components/views/audio_messages/AudioPlayer.tsx
+++ b/src/components/views/audio_messages/AudioPlayer.tsx
@@ -101,11 +101,11 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
                 />
                 <div className='mx_AudioPlayer_mediaInfo'>
                     <span className='mx_AudioPlayer_mediaName'>
-                        {this.props.mediaName || _t("Unnamed audio")}
+                        { this.props.mediaName || _t("Unnamed audio") }
                     </span>
                     <div className='mx_AudioPlayer_byline'>
                         <DurationClock playback={this.props.playback} />
-                        &nbsp; {/* easiest way to introduce a gap between the components */}
+                        &nbsp; { /* easiest way to introduce a gap between the components */ }
                         { this.renderFileSize() }
                     </div>
                 </div>
diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx
index 7f387715f8..cb1a179f2e 100644
--- a/src/components/views/audio_messages/Clock.tsx
+++ b/src/components/views/audio_messages/Clock.tsx
@@ -43,6 +43,6 @@ export default class Clock extends React.Component<IProps, IState> {
     public render() {
         const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0');
         const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis
-        return <span className='mx_Clock'>{minutes}:{seconds}</span>;
+        return <span className='mx_Clock'>{ minutes }:{ seconds }</span>;
     }
 }
diff --git a/src/components/views/audio_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx
index 3b7a881754..8a4427fd01 100644
--- a/src/components/views/audio_messages/Waveform.tsx
+++ b/src/components/views/audio_messages/Waveform.tsx
@@ -47,7 +47,7 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
 
     public render() {
         return <div className='mx_Waveform'>
-            {this.props.relHeights.map((h, i) => {
+            { this.props.relHeights.map((h, i) => {
                 const progress = this.props.progress;
                 const isCompleteBar = (i / this.props.relHeights.length) <= progress && progress > 0;
                 const classes = classNames({
@@ -57,7 +57,7 @@ export default class Waveform extends React.PureComponent<IProps, IState> {
                 return <span key={i} style={{
                     "--barHeight": h,
                 } as WaveformCSSProperties} className={classes} />;
-            })}
+            }) }
         </div>;
     }
 }
diff --git a/src/components/views/auth/AuthPage.tsx b/src/components/views/auth/AuthPage.tsx
index 68f3255c2c..c402d5b699 100644
--- a/src/components/views/auth/AuthPage.tsx
+++ b/src/components/views/auth/AuthPage.tsx
@@ -26,7 +26,7 @@ export default class AuthPage extends React.PureComponent {
         return (
             <div className="mx_AuthPage">
                 <div className="mx_AuthPage_modal">
-                    {this.props.children}
+                    { this.props.children }
                 </div>
                 <AuthFooter />
             </div>
diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx
index d71d8a6b15..b1c09f2b22 100644
--- a/src/components/views/auth/CaptchaForm.tsx
+++ b/src/components/views/auth/CaptchaForm.tsx
@@ -21,59 +21,68 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
 
 const DIV_ID = 'mx_recaptcha';
 
-interface IProps {
-    sitePublicKey?: string;
+interface ICaptchaFormProps {
+    sitePublicKey: string;
     onCaptchaResponse: (response: string) => void;
 }
 
-interface IState {
-    errorText: string;
+interface ICaptchaFormState {
+    errorText?: string;
+
 }
 
 /**
  * A pure UI component which displays a captcha form.
  */
 @replaceableComponent("views.auth.CaptchaForm")
-export default class CaptchaForm extends React.Component<IProps, IState> {
-    private captchaWidgetId: string;
-    private recaptchaContainer = createRef<HTMLDivElement>();
+export default class CaptchaForm extends React.Component<ICaptchaFormProps, ICaptchaFormState> {
     static defaultProps = {
         onCaptchaResponse: () => {},
     };
 
-    constructor(props) {
+    private captchaWidgetId?: string;
+    private recaptchaContainer = createRef<HTMLDivElement>();
+
+    constructor(props: ICaptchaFormProps) {
         super(props);
 
         this.state = {
-            errorText: null,
+            errorText: undefined,
         };
 
         CountlyAnalytics.instance.track("onboarding_grecaptcha_begin");
     }
 
-    public componentDidMount(): void {
+    componentDidMount() {
         // Just putting a script tag into the returned jsx doesn't work, annoyingly,
         // so we do this instead.
-        if (window.grecaptcha) { // TODO: Properly find the type of `grecaptcha`
+        if (this.isRecaptchaReady()) {
             // already loaded
             this.onCaptchaLoaded();
         } else {
             console.log("Loading recaptcha script...");
-            window.mx_on_recaptcha_loaded = () => {this.onCaptchaLoaded();};
+            window.mxOnRecaptchaLoaded = () => { this.onCaptchaLoaded(); };
             const scriptTag = document.createElement('script');
             scriptTag.setAttribute(
-                'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
+                'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mxOnRecaptchaLoaded&render=explicit`,
             );
             this.recaptchaContainer.current.appendChild(scriptTag);
         }
     }
 
-    public componentWillUnmount(): void {
+    componentWillUnmount() {
         this.resetRecaptcha();
     }
 
-    private renderRecaptcha(divId): void {
-        if (!window.grecaptcha) {
+    // Borrowed directly from: https://github.com/codeep/react-recaptcha-google/commit/e118fa5670fa268426969323b2e7fe77698376ba
+    private isRecaptchaReady(): boolean {
+        return typeof window !== "undefined" &&
+            typeof global.grecaptcha !== "undefined" &&
+            typeof global.grecaptcha.render === 'function';
+    }
+
+    private renderRecaptcha(divId: string) {
+        if (!this.isRecaptchaReady()) {
             console.error("grecaptcha not loaded!");
             throw new Error("Recaptcha did not load successfully");
         }
@@ -83,23 +92,23 @@ export default class CaptchaForm extends React.Component<IProps, IState> {
             console.error("No public key for recaptcha!");
             throw new Error(
                 "This server has not supplied enough information for Recaptcha "
-                    + "authentication");
+                + "authentication");
         }
 
         console.info("Rendering to %s", divId);
-        this.captchaWidgetId = window.grecaptcha.render(divId, {
+        this.captchaWidgetId = global.grecaptcha.render(divId, {
             sitekey: publicKey,
             callback: this.props.onCaptchaResponse,
         });
     }
 
-    private resetRecaptcha(): void {
+    private resetRecaptcha() {
         if (this.captchaWidgetId !== null) {
-            window.grecaptcha.reset(this.captchaWidgetId);
+            global.grecaptcha.reset(this.captchaWidgetId);
         }
     }
 
-    private onCaptchaLoaded(): void {
+    private onCaptchaLoaded() {
         console.log("Loaded recaptcha script.");
         try {
             this.renderRecaptcha(DIV_ID);
@@ -116,7 +125,7 @@ export default class CaptchaForm extends React.Component<IProps, IState> {
         }
     }
 
-    public render(): React.ReactNode {
+    render() {
         let error = null;
         if (this.state.errorText) {
             error = (
@@ -128,9 +137,9 @@ export default class CaptchaForm extends React.Component<IProps, IState> {
 
         return (
             <div ref={this.recaptchaContainer}>
-                <p>{_t(
+                <p>{ _t(
                     "This homeserver would like to make sure you are not a robot.",
-                )}</p>
+                ) }</p>
                 <div id={DIV_ID} />
                 { error }
             </div>
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx
index d9af2c2b77..763ce10cd9 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx
@@ -417,12 +417,12 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
         if (this.props.showContinue !== false) {
             // XXX: button classes
             submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
-                onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
+                onClick={this.trySubmit} disabled={!allChecked}>{ _t("Accept") }</button>;
         }
 
         return (
             <div>
-                <p>{_t("Please review and accept the policies of this homeserver:")}</p>
+                <p>{ _t("Please review and accept the policies of this homeserver:") }</p>
                 { checkboxes }
                 { errorSection }
                 { submitButton }
@@ -613,7 +613,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
                                 className="mx_InteractiveAuthEntryComponents_msisdnEntry"
                                 value={this.state.token}
                                 onChange={this.onTokenChange}
-                                aria-label={ _t("Code")}
+                                aria-label={_t("Code")}
                             />
                             <br />
                             <input type="submit" value={_t("Submit")}
@@ -621,7 +621,7 @@ export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsi
                                 disabled={!enableSubmit}
                             />
                         </form>
-                        {errorSection}
+                        { errorSection }
                     </div>
                 </div>
             );
@@ -717,21 +717,21 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
             <AccessibleButton
                 onClick={this.props.onCancel}
                 kind={this.props.continueKind ? (this.props.continueKind + '_outline') : 'primary_outline'}
-            >{_t("Cancel")}</AccessibleButton>
+            >{ _t("Cancel") }</AccessibleButton>
         );
         if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) {
             continueButton = (
                 <AccessibleButton
                     onClick={this.onStartAuthClick}
                     kind={this.props.continueKind || 'primary'}
-                >{this.props.continueText || _t("Single Sign On")}</AccessibleButton>
+                >{ this.props.continueText || _t("Single Sign On") }</AccessibleButton>
             );
         } else {
             continueButton = (
                 <AccessibleButton
                     onClick={this.onConfirmClick}
                     kind={this.props.continueKind || 'primary'}
-                >{this.props.continueText || _t("Confirm")}</AccessibleButton>
+                >{ this.props.continueText || _t("Confirm") }</AccessibleButton>
             );
         }
 
@@ -753,8 +753,8 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
         return <React.Fragment>
             { errorSection }
             <div className="mx_InteractiveAuthEntryComponents_sso_buttons">
-                {cancelButton}
-                {continueButton}
+                { cancelButton }
+                { continueButton }
             </div>
         </React.Fragment>;
     }
@@ -825,7 +825,7 @@ export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
                 <a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
                     _t("Start authentication")
                 }</a>
-                {errorSection}
+                { errorSection }
             </div>
         );
     }
diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx
index a77dd0b683..587d7f2453 100644
--- a/src/components/views/auth/PasswordLogin.tsx
+++ b/src/components/views/auth/PasswordLogin.tsx
@@ -416,7 +416,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
                 kind="link"
                 onClick={this.onForgotPasswordClick}
             >
-                {_t("Forgot password?")}
+                { _t("Forgot password?") }
             </AccessibleButton>;
         }
 
@@ -441,16 +441,16 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
                         disabled={this.props.disableSubmit}
                     >
                         <option key={LoginField.MatrixId} value={LoginField.MatrixId}>
-                            {_t('Username')}
+                            { _t('Username') }
                         </option>
                         <option
                             key={LoginField.Email}
                             value={LoginField.Email}
                         >
-                            {_t('Email address')}
+                            { _t('Email address') }
                         </option>
                         <option key={LoginField.Password} value={LoginField.Password}>
-                            {_t('Phone')}
+                            { _t('Phone') }
                         </option>
                     </Field>
                 </div>
@@ -460,8 +460,8 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
         return (
             <div>
                 <form onSubmit={this.onSubmitForm}>
-                    {loginType}
-                    {loginField}
+                    { loginType }
+                    { loginField }
                     <Field
                         className={pwFieldClass}
                         type="password"
@@ -474,7 +474,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
                         onValidate={this.onPasswordValidate}
                         ref={field => this[LoginField.Password] = field}
                     />
-                    {forgotPasswordJsx}
+                    { forgotPasswordJsx }
                     { !this.props.busy && <input className="mx_Login_submit"
                         type="submit"
                         value={_t('Sign in')}
diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx
index 25ea347d24..8a0fb34f3c 100644
--- a/src/components/views/auth/RegistrationForm.tsx
+++ b/src/components/views/auth/RegistrationForm.tsx
@@ -537,15 +537,15 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
             <div>
                 <form onSubmit={this.onSubmit}>
                     <div className="mx_AuthBody_fieldRow">
-                        {this.renderUsername()}
+                        { this.renderUsername() }
                     </div>
                     <div className="mx_AuthBody_fieldRow">
-                        {this.renderPassword()}
-                        {this.renderPasswordConfirm()}
+                        { this.renderPassword() }
+                        { this.renderPasswordConfirm() }
                     </div>
                     <div className="mx_AuthBody_fieldRow">
-                        {this.renderEmail()}
-                        {this.renderPhoneNumber()}
+                        { this.renderEmail() }
+                        { this.renderPhoneNumber() }
                     </div>
                     { emailHelperText }
                     { registerButton }
diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx
index 5e6bf45f07..99f2b70efc 100644
--- a/src/components/views/avatars/DecoratedRoomAvatar.tsx
+++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx
@@ -205,8 +205,8 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
                 oobData={this.props.oobData}
                 viewAvatarOnClick={this.props.viewAvatarOnClick}
             />
-            {icon}
-            {badge}
+            { icon }
+            { badge }
         </div>;
     }
 }
diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js
index b8b23dc33e..82b7b8e400 100644
--- a/src/components/views/avatars/MemberStatusMessageAvatar.js
+++ b/src/components/views/avatars/MemberStatusMessageAvatar.js
@@ -145,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
                 isExpanded={this.state.menuDisplayed}
                 label={_t("User Status")}
             >
-                {avatar}
+                { avatar }
             </ContextMenuButton>
 
             { contextMenu }
diff --git a/src/components/views/context_menus/CallContextMenu.tsx b/src/components/views/context_menus/CallContextMenu.tsx
index 76e1670669..a61cdeedd3 100644
--- a/src/components/views/context_menus/CallContextMenu.tsx
+++ b/src/components/views/context_menus/CallContextMenu.tsx
@@ -65,15 +65,15 @@ export default class CallContextMenu extends React.Component<IProps> {
         let transferItem;
         if (this.props.call.opponentCanBeTransferred()) {
             transferItem = <MenuItem className="mx_CallContextMenu_item" onClick={this.onTransferClick}>
-                {_t("Transfer")}
+                { _t("Transfer") }
             </MenuItem>;
         }
 
         return <ContextMenu {...this.props}>
             <MenuItem className="mx_CallContextMenu_item" onClick={handler}>
-                {holdUnholdCaption}
+                { holdUnholdCaption }
             </MenuItem>
-            {transferItem}
+            { transferItem }
         </ContextMenu>;
     }
 }
diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx
index a9c75bf3ba..1d822fd246 100644
--- a/src/components/views/context_menus/IconizedContextMenu.tsx
+++ b/src/components/views/context_menus/IconizedContextMenu.tsx
@@ -64,8 +64,8 @@ export const IconizedContextMenuRadio: React.FC<IRadioProps> = ({
         label={label}
     >
         <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
-        <span className="mx_IconizedContextMenu_label">{label}</span>
-        {active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" />}
+        <span className="mx_IconizedContextMenu_label">{ label }</span>
+        { active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" /> }
     </MenuItemRadio>;
 };
 
@@ -85,15 +85,15 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
         label={label}
     >
         <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
-        <span className="mx_IconizedContextMenu_label">{label}</span>
-        {active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" />}
+        <span className="mx_IconizedContextMenu_label">{ label }</span>
+        { active && <span className="mx_IconizedContextMenu_icon mx_IconizedContextMenu_checked" /> }
     </MenuItemCheckbox>;
 };
 
 export const IconizedContextMenuOption: React.FC<IOptionProps> = ({ label, iconClassName, ...props }) => {
     return <MenuItem {...props} label={label}>
         { iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
-        <span className="mx_IconizedContextMenu_label">{label}</span>
+        <span className="mx_IconizedContextMenu_label">{ label }</span>
     </MenuItem>;
 };
 
@@ -104,7 +104,7 @@ export const IconizedContextMenuOptionList: React.FC<IOptionListProps> = ({ firs
     });
 
     return <div className={classes}>
-        {children}
+        { children }
     </div>;
 };
 
diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx
index 999e98f4ad..8f5d3baa17 100644
--- a/src/components/views/context_menus/MessageContextMenu.tsx
+++ b/src/components/views/context_menus/MessageContextMenu.tsx
@@ -43,11 +43,15 @@ export function canCancel(eventStatus: EventStatus): boolean {
     return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
 }
 
-interface IEventTileOps {
+export interface IEventTileOps {
     isWidgetHidden(): boolean;
     unhideWidget(): void;
 }
 
+export interface IOperableEventTile {
+    getEventTileOps(): IEventTileOps;
+}
+
 interface IProps {
     /* the MatrixEvent associated with the context menu */
     mxEvent: MatrixEvent;
@@ -268,7 +272,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
                 resendReactionsButton = (
                     <IconizedContextMenuOption
                         iconClassName="mx_MessageContextMenu_iconResend"
-                        label={ _t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount }) }
+                        label={_t('Resend %(unsentCount)s reaction(s)', { unsentCount: unsentReactionsCount })}
                         onClick={this.onResendReactionsClick}
                     />
                 );
@@ -298,7 +302,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
                 pinButton = (
                     <IconizedContextMenuOption
                         iconClassName="mx_MessageContextMenu_iconPin"
-                        label={ this.isPinned() ? _t('Unpin') : _t('Pin') }
+                        label={this.isPinned() ? _t('Unpin') : _t('Pin')}
                         onClick={this.onPinClick}
                     />
                 );
@@ -333,7 +337,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
             <IconizedContextMenuOption
                 iconClassName="mx_MessageContextMenu_iconPermalink"
                 onClick={this.onPermalinkClick}
-                label= {_t('Share')}
+                label={_t('Share')}
                 element="a"
                 {
                     // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
@@ -364,7 +368,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
                 <IconizedContextMenuOption
                     iconClassName="mx_MessageContextMenu_iconLink"
                     onClick={this.closeMenu}
-                    label={ _t('Source URL') }
+                    label={_t('Source URL')}
                     element="a"
                     {
                         // XXX: Typescript signature for AccessibleButton doesn't work properly for non-inputs like `a`
diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js
index 23b91fe68f..f90d9cc005 100644
--- a/src/components/views/context_menus/StatusMessageContextMenu.js
+++ b/src/components/views/context_menus/StatusMessageContextMenu.js
@@ -99,20 +99,20 @@ export default class StatusMessageContextMenu extends React.Component {
                 actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
                     onClick={this._onClearClick}
                 >
-                    <span>{_t("Clear status")}</span>
+                    <span>{ _t("Clear status") }</span>
                 </AccessibleButton>;
             } else {
                 actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
                     onClick={this._onSubmit}
                 >
-                    <span>{_t("Update status")}</span>
+                    <span>{ _t("Update status") }</span>
                 </AccessibleButton>;
             }
         } else {
             actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
                 disabled={!this.state.message} onClick={this._onSubmit}
             >
-                <span>{_t("Set status")}</span>
+                <span>{ _t("Set status") }</span>
             </AccessibleButton>;
         }
 
@@ -130,8 +130,8 @@ export default class StatusMessageContextMenu extends React.Component {
                 onChange={this._onStatusChange}
             />
             <div className="mx_StatusMessageContextMenu_actionContainer">
-                {actionButton}
-                {spinner}
+                { actionButton }
+                { spinner }
             </div>
         </form>;
 
diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
index 5024b98def..0f78b971eb 100644
--- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
+++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
@@ -221,7 +221,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
     return <div className="mx_AddExistingToSpace">
         <SearchBox
             className="mx_textinput_icon mx_textinput_search"
-            placeholder={ _t("Filter your rooms and spaces") }
+            placeholder={_t("Filter your rooms and spaces")}
             onSearch={setQuery}
             autoComplete={true}
             autoFocus={true}
diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js
index 09714e24e3..61215c33a1 100644
--- a/src/components/views/dialogs/AddressPickerDialog.js
+++ b/src/components/views/dialogs/AddressPickerDialog.js
@@ -620,7 +620,7 @@ export default class AddressPickerDialog extends React.Component {
         let inputLabel;
         if (this.props.description) {
             inputLabel = <div className="mx_AddressPickerDialog_label">
-                <label htmlFor="textinput">{this.props.description}</label>
+                <label htmlFor="textinput">{ this.props.description }</label>
             </div>;
         }
 
@@ -690,7 +690,7 @@ export default class AddressPickerDialog extends React.Component {
             && this.props.validAddressTypes.includes('email')) {
             const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
             if (defaultIdentityServerUrl) {
-                identityServer = <div className="mx_AddressPickerDialog_identityServer">{_t(
+                identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
                     "Use an identity server to invite by email. " +
                     "<default>Use the default (%(defaultIdentityServerName)s)</default> " +
                     "or manage in <settings>Settings</settings>.",
@@ -698,25 +698,25 @@ export default class AddressPickerDialog extends React.Component {
                         defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
                     },
                     {
-                        default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
-                        settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
+                        default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{ sub }</a>,
+                        settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
                     },
-                )}</div>;
+                ) }</div>;
             } else {
-                identityServer = <div className="mx_AddressPickerDialog_identityServer">{_t(
+                identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
                     "Use an identity server to invite by email. " +
                     "Manage in <settings>Settings</settings>.",
                     {}, {
-                        settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
+                        settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
                     },
-                )}</div>;
+                ) }</div>;
             }
         }
 
         return (
             <BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown}
                 onFinished={this.props.onFinished} title={this.props.title}>
-                {inputLabel}
+                { inputLabel }
                 <div className="mx_Dialog_content">
                     <div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
                     { error }
diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.tsx b/src/components/views/dialogs/AskInviteAnywayDialog.tsx
index 26fad0c724..3ae82f1026 100644
--- a/src/components/views/dialogs/AskInviteAnywayDialog.tsx
+++ b/src/components/views/dialogs/AskInviteAnywayDialog.tsx
@@ -51,7 +51,7 @@ export default class AskInviteAnywayDialog extends React.Component<IProps> {
 
     public render() {
         const errorList = this.props.unknownProfileUsers
-            .map(address => <li key={address.userId}>{address.userId}: {address.errorText}</li>);
+            .map(address => <li key={address.userId}>{ address.userId }: { address.errorText }</li>);
 
         return (
             <BaseDialog className='mx_RetryInvitesDialog'
@@ -60,8 +60,8 @@ export default class AskInviteAnywayDialog extends React.Component<IProps> {
                 contentId='mx_Dialog_content'
             >
                 <div id='mx_Dialog_content'>
-                    {/* eslint-disable-next-line */}
-                    <p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p>
+                    <p>{ _t("Unable to find profiles for the Matrix IDs listed below - " +
+                        "would you like to invite them anyway?") }</p>
                     <ul>
                         { errorList }
                     </ul>
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js
index e92bd6315e..8ccc485d7c 100644
--- a/src/components/views/dialogs/BaseDialog.js
+++ b/src/components/views/dialogs/BaseDialog.js
@@ -149,7 +149,7 @@ export default class BaseDialog extends React.Component {
                         'mx_Dialog_headerWithCancel': !!cancelButton,
                     })}>
                         <div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
-                            {headerImage}
+                            { headerImage }
                             { this.props.title }
                         </div>
                         { this.props.headerButton }
diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx
index 5a2f16f169..917004dbc7 100644
--- a/src/components/views/dialogs/BetaFeedbackDialog.tsx
+++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx
@@ -69,7 +69,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
             <div className="mx_BetaFeedbackDialog_subheading">
                 { _t(info.feedbackSubheading) }
                 &nbsp;
-                { _t("Your platform and username will be noted to help us use your feedback as much as we can.")}
+                { _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
 
                 <AccessibleButton kind="link" onClick={() => {
                     onFinished(false);
diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx
index 6baf24f797..64e984fe20 100644
--- a/src/components/views/dialogs/BugReportDialog.tsx
+++ b/src/components/views/dialogs/BugReportDialog.tsx
@@ -166,7 +166,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
         let error = null;
         if (this.state.err) {
             error = <div className="error">
-                {this.state.err}
+                { this.state.err }
             </div>;
         }
 
@@ -175,7 +175,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
             progress = (
                 <div className="progress">
                     <Spinner />
-                    {this.state.progress} ...
+                    { this.state.progress } ...
                 </div>
             );
         }
@@ -221,7 +221,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
                         <AccessibleButton onClick={this.onDownload} kind="link" disabled={this.state.downloadBusy}>
                             { _t("Download logs") }
                         </AccessibleButton>
-                        {this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>}
+                        { this.state.downloadProgress && <span>{ this.state.downloadProgress } ...</span> }
                     </div>
 
                     <Field
@@ -246,8 +246,8 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
                             "please include those things here.",
                         )}
                     />
-                    {progress}
-                    {error}
+                    { progress }
+                    { error }
                 </div>
                 <DialogButtons primaryButton={_t("Send logs")}
                     onPrimaryButtonClick={this.onSubmit}
diff --git a/src/components/views/dialogs/ChangelogDialog.tsx b/src/components/views/dialogs/ChangelogDialog.tsx
index d484d94249..de9e454401 100644
--- a/src/components/views/dialogs/ChangelogDialog.tsx
+++ b/src/components/views/dialogs/ChangelogDialog.tsx
@@ -59,7 +59,7 @@ export default class ChangelogDialog extends React.Component<IProps> {
         return (
             <li key={commit.sha} className="mx_ChangelogDialog_li">
                 <a href={commit.html_url} target="_blank" rel="noreferrer noopener">
-                    {commit.commit.message.split('\n')[0]}
+                    { commit.commit.message.split('\n')[0] }
                 </a>
             </li>
         );
@@ -79,15 +79,15 @@ export default class ChangelogDialog extends React.Component<IProps> {
             }
             return (
                 <div key={repo}>
-                    <h2>{repo}</h2>
-                    <ul>{content}</ul>
+                    <h2>{ repo }</h2>
+                    <ul>{ content }</ul>
                 </div>
             );
         });
 
         const content = (
             <div className="mx_ChangelogDialog_content">
-                {this.props.version == null || this.props.newVersion == null ? <h2>{_t("Unavailable")}</h2> : logs}
+                { this.props.version == null || this.props.newVersion == null ? <h2>{ _t("Unavailable") }</h2> : logs }
             </div>
         );
 
diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
index 7627489deb..73fd4def25 100644
--- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
+++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
@@ -156,8 +156,8 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
                     height={avatarSize}
                 />
                 <div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
-                    <span className="mx_CommunityPrototypeInviteDialog_personName">{person.user.name}</span>
-                    <span className="mx_CommunityPrototypeInviteDialog_personId">{person.userId}</span>
+                    <span className="mx_CommunityPrototypeInviteDialog_personName">{ person.user.name }</span>
+                    <span className="mx_CommunityPrototypeInviteDialog_personId">{ person.userId }</span>
                 </div>
                 <StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
             </div>
@@ -187,7 +187,7 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
         emailAddresses.push((
             <Field
                 key={emailAddresses.length}
-                value={""}
+                value=""
                 onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
                 label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
                 placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
@@ -207,16 +207,16 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
                         onClick={this.onShowMorePeople}
                         kind="link" key="more"
                         className="mx_CommunityPrototypeInviteDialog_morePeople"
-                    >{_t("Show more")}</AccessibleButton>
+                    >{ _t("Show more") }</AccessibleButton>
                 ));
             }
         }
         if (this.state.people.length > 0) {
             peopleIntro = (
                 <div className="mx_CommunityPrototypeInviteDialog_people">
-                    <span>{_t("People you know on %(brand)s", { brand: SdkConfig.get().brand })}</span>
+                    <span>{ _t("People you know on %(brand)s", { brand: SdkConfig.get().brand }) }</span>
                     <AccessibleButton onClick={this.onShowPeopleClick}>
-                        {this.state.showPeople ? _t("Hide") : _t("Show")}
+                        { this.state.showPeople ? _t("Hide") : _t("Show") }
                     </AccessibleButton>
                 </div>
             );
@@ -236,14 +236,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent<
             >
                 <form onSubmit={this.onSubmit}>
                     <div className="mx_Dialog_content">
-                        {emailAddresses}
-                        {peopleIntro}
-                        {people}
+                        { emailAddresses }
+                        { peopleIntro }
+                        { people }
                         <AccessibleButton
                             kind="primary" onClick={this.onSubmit}
                             disabled={this.state.busy}
                             className="mx_CommunityPrototypeInviteDialog_primaryButton"
-                        >{buttonText}</AccessibleButton>
+                        >{ buttonText }</AccessibleButton>
                     </div>
                 </form>
             </BaseDialog>
diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx
index 544d0df1c9..2577d5456d 100644
--- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx
+++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx
@@ -44,10 +44,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
             >
                 <div className='mx_ConfirmWipeDeviceDialog_content'>
                     <p>
-                        {_t(
+                        { _t(
                             "Clearing all data from this session is permanent. Encrypted messages will be lost " +
                             "unless their keys have been backed up.",
-                        )}
+                        ) }
                     </p>
                 </div>
                 <DialogButtons
diff --git a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
index 29e9a2ad39..392ab9edad 100644
--- a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
+++ b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
@@ -144,11 +144,11 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
         if (this.state.localpart) {
             communityId = (
                 <span className="mx_CreateCommunityPrototypeDialog_communityId">
-                    {_t("Community ID: +<localpart />:%(domain)s", {
+                    { _t("Community ID: +<localpart />:%(domain)s", {
                         domain: MatrixClientPeg.getHomeserverName(),
                     }, {
-                        localpart: () => <u>{this.state.localpart}</u>,
-                    })}
+                        localpart: () => <u>{ this.state.localpart }</u>,
+                    }) }
                     <InfoTooltip
                         tooltip={_t(
                             "Use this when referencing your community to others. The community ID " +
@@ -161,14 +161,14 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
 
         let helpText = (
             <span className="mx_CreateCommunityPrototypeDialog_subtext">
-                {_t("You can change this later if needed.")}
+                { _t("You can change this later if needed.") }
             </span>
         );
         if (this.state.error) {
             const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error";
             helpText = (
                 <span className={classes}>
-                    {this.state.error}
+                    { this.state.error }
                 </span>
             );
         }
@@ -193,13 +193,13 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
                                 placeholder={_t("Enter name")}
                                 label={_t("Enter name")}
                             />
-                            {helpText}
+                            { helpText }
                             <span className="mx_CreateCommunityPrototypeDialog_subtext">
-                                {/*nbsp is to reserve the height of this element when there's nothing*/}
-                                &nbsp;{communityId}
+                                { /*nbsp is to reserve the height of this element when there's nothing*/ }
+                                &nbsp;{ communityId }
                             </span>
                             <AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
-                                {_t("Create")}
+                                { _t("Create") }
                             </AccessibleButton>
                         </div>
                         <div className="mx_CreateCommunityPrototypeDialog_colAvatar">
@@ -212,12 +212,12 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
                                 onClick={this.onChangeAvatar}
                                 className="mx_CreateCommunityPrototypeDialog_avatarContainer"
                             >
-                                {preview}
+                                { preview }
                             </AccessibleButton>
                             <div className="mx_CreateCommunityPrototypeDialog_tip">
-                                <b>{_t("Add image (optional)")}</b>
+                                <b>{ _t("Add image (optional)") }</b>
                                 <span>
-                                    {_t("An image will help people identify your community.")}
+                                    { _t("An image will help people identify your community.") }
                                 </span>
                             </div>
                         </div>
diff --git a/src/components/views/dialogs/CreateGroupDialog.tsx b/src/components/views/dialogs/CreateGroupDialog.tsx
index d6bb582079..88ae801441 100644
--- a/src/components/views/dialogs/CreateGroupDialog.tsx
+++ b/src/components/views/dialogs/CreateGroupDialog.tsx
@@ -102,7 +102,7 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
         });
     };
 
-    _onCancel = () => {
+    private onCancel = () => {
         this.props.onFinished(false);
     };
 
@@ -167,7 +167,7 @@ export default class CreateGroupDialog extends React.Component<IProps, IState> {
                     </div>
                     <div className="mx_Dialog_buttons">
                         <input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
-                        <button onClick={this._onCancel}>
+                        <button onClick={this.onCancel}>
                             { _t("Cancel") }
                         </button>
                     </div>
diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx
index b5c0096771..6d75b94c70 100644
--- a/src/components/views/dialogs/CreateRoomDialog.tsx
+++ b/src/components/views/dialogs/CreateRoomDialog.tsx
@@ -224,15 +224,15 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
             );
         }
 
-        let publicPrivateLabel = <p>{_t(
+        let publicPrivateLabel = <p>{ _t(
             "Private rooms can be found and joined by invitation only. Public rooms can be " +
             "found and joined by anyone.",
-        )}</p>;
+        ) }</p>;
         if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
-            publicPrivateLabel = <p>{_t(
+            publicPrivateLabel = <p>{ _t(
                 "Private rooms can be found and joined by invitation only. Public rooms can be " +
                 "found and joined by anyone in this community.",
-            )}</p>;
+            ) }</p>;
         }
 
         let e2eeSection;
@@ -250,7 +250,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
             }
             e2eeSection = <React.Fragment>
                 <LabelledToggleSwitch
-                    label={ _t("Enable end-to-end encryption")}
+                    label={_t("Enable end-to-end encryption")}
                     onChange={this.onEncryptedChange}
                     value={this.state.isEncrypted}
                     className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
@@ -318,7 +318,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
                                 onChange={this.onNoFederateChange}
                                 value={this.state.noFederate}
                             />
-                            <p>{federateLabel}</p>
+                            <p>{ federateLabel }</p>
                         </details>
                     </div>
                 </form>
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx
index b2ac849314..7221df222f 100644
--- a/src/components/views/dialogs/DeactivateAccountDialog.tsx
+++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx
@@ -172,11 +172,11 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
             </div>;
         }
 
-        let auth = <div>{_t("Loading...")}</div>;
+        let auth = <div>{ _t("Loading...") }</div>;
         if (this.state.authData && this.state.authEnabled) {
             auth = (
                 <div>
-                    {this.state.bodyText}
+                    { this.state.bodyText }
                     <InteractiveAuth
                         matrixClient={MatrixClientPeg.get()}
                         authData={this.state.authData}
@@ -230,18 +230,18 @@ export default class DeactivateAccountDialog extends React.Component<IProps, ISt
                                 checked={this.state.shouldErase}
                                 onChange={this.onEraseFieldChange}
                             >
-                                {_t(
+                                { _t(
                                     "Please forget all messages I have sent when my account is deactivated " +
                                     "(<b>Warning:</b> this will cause future users to see an incomplete view " +
                                     "of conversations)",
                                     {},
                                     { b: (sub) => <b>{ sub }</b> },
-                                )}
+                                ) }
                             </StyledCheckbox>
                         </p>
 
-                        {error}
-                        {auth}
+                        { error }
+                        { auth }
                     </div>
 
                 </div>
diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx
index 86b8f93d7b..61cda796ee 100644
--- a/src/components/views/dialogs/DevtoolsDialog.tsx
+++ b/src/components/views/dialogs/DevtoolsDialog.tsx
@@ -337,7 +337,7 @@ class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredList
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
+    UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line
         if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
         this.setState({
             filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
@@ -494,7 +494,7 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
                         }
 
                         return <button className={classes} key={eventType} onClick={onClickFn}>
-                            {eventType}
+                            { eventType }
                         </button>;
                     })
                 }
@@ -726,17 +726,17 @@ const VerificationRequestExplorer: React.FC<{
     return (<div className="mx_DevTools_VerificationRequest">
         <dl>
             <dt>Transaction</dt>
-            <dd>{txnId}</dd>
+            <dd>{ txnId }</dd>
             <dt>Phase</dt>
-            <dd>{PHASE_MAP[request.phase] || request.phase}</dd>
+            <dd>{ PHASE_MAP[request.phase] || request.phase }</dd>
             <dt>Timeout</dt>
-            <dd>{Math.floor(timeout / 1000)}</dd>
+            <dd>{ Math.floor(timeout / 1000) }</dd>
             <dt>Methods</dt>
-            <dd>{request.methods && request.methods.join(", ")}</dd>
+            <dd>{ request.methods && request.methods.join(", ") }</dd>
             <dt>requestingUserId</dt>
-            <dd>{request.requestingUserId}</dd>
+            <dd>{ request.requestingUserId }</dd>
             <dt>observeOnly</dt>
-            <dd>{JSON.stringify(request.observeOnly)}</dd>
+            <dd>{ JSON.stringify(request.observeOnly) }</dd>
         </dl>
     </div>);
 };
@@ -771,12 +771,12 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
 
         return (<div>
             <div className="mx_Dialog_content">
-                {Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
+                { Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
                     <VerificationRequestExplorer txnId={txnId} request={request} key={txnId} />,
-                )}
+                ) }
             </div>
             <div className="mx_Dialog_buttons">
-                <button onClick={this.props.onBack}>{_t("Back")}</button>
+                <button onClick={this.props.onBack}>{ _t("Back") }</button>
             </div>
         </div>);
     }
@@ -844,9 +844,9 @@ class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerStat
             const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
             if (!stateEv) { // "should never happen"
                 return <div>
-                    {_t("There was an error finding this widget.")}
+                    { _t("There was an error finding this widget.") }
                     <div className="mx_Dialog_buttons">
-                        <button onClick={this.onBack}>{_t("Back")}</button>
+                        <button onClick={this.onBack}>{ _t("Back") }</button>
                     </div>
                 </div>;
             }
@@ -865,17 +865,17 @@ class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerStat
         return (<div>
             <div className="mx_Dialog_content">
                 <FilteredList query={this.state.query} onChange={this.onQueryChange}>
-                    {widgets.map(w => {
+                    { widgets.map(w => {
                         return <button
                             className='mx_DevTools_RoomStateExplorer_button'
                             key={w.url + w.eventId}
                             onClick={() => this.onEditWidget(w)}
-                        >{w.url}</button>;
-                    })}
+                        >{ w.url }</button>;
+                    }) }
                 </FilteredList>
             </div>
             <div className="mx_Dialog_buttons">
-                <button onClick={this.onBack}>{_t("Back")}</button>
+                <button onClick={this.onBack}>{ _t("Back") }</button>
             </div>
         </div>);
     }
@@ -1007,7 +1007,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
     private renderCanEditLevel(roomId: string, level: SettingLevel): React.ReactNode {
         const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
         const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
-        return <td className={className}><code>{canEdit.toString()}</code></td>;
+        return <td className={className}><code>{ canEdit.toString() }</code></td>;
     }
 
     render() {
@@ -1028,17 +1028,17 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
                         <table>
                             <thead>
                                 <tr>
-                                    <th>{_t("Setting ID")}</th>
-                                    <th>{_t("Value")}</th>
-                                    <th>{_t("Value in this room")}</th>
+                                    <th>{ _t("Setting ID") }</th>
+                                    <th>{ _t("Value") }</th>
+                                    <th>{ _t("Value in this room") }</th>
                                 </tr>
                             </thead>
                             <tbody>
-                                {allSettings.map(i => (
+                                { allSettings.map(i => (
                                     <tr key={i}>
                                         <td>
                                             <a href="" onClick={(e) => this.onViewClick(e, i)}>
-                                                <code>{i}</code>
+                                                <code>{ i }</code>
                                             </a>
                                             <a href="" onClick={(e) => this.onEditClick(e, i)}
                                                 className='mx_DevTools_SettingsExplorer_edit'
@@ -1047,20 +1047,20 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
                                             </a>
                                         </td>
                                         <td>
-                                            <code>{this.renderSettingValue(SettingsStore.getValue(i))}</code>
+                                            <code>{ this.renderSettingValue(SettingsStore.getValue(i)) }</code>
                                         </td>
                                         <td>
                                             <code>
-                                                {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))}
+                                                { this.renderSettingValue(SettingsStore.getValue(i, room.roomId)) }
                                             </code>
                                         </td>
                                     </tr>
-                                ))}
+                                )) }
                             </tbody>
                         </table>
                     </div>
                     <div className="mx_Dialog_buttons">
-                        <button onClick={this.onBack}>{_t("Back")}</button>
+                        <button onClick={this.onBack}>{ _t("Back") }</button>
                     </div>
                 </div>
             );
@@ -1068,36 +1068,36 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
             return (
                 <div>
                     <div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
-                        <h3>{_t("Setting:")} <code>{this.state.editSetting}</code></h3>
+                        <h3>{ _t("Setting:") } <code>{ this.state.editSetting }</code></h3>
 
                         <div className='mx_DevTools_SettingsExplorer_warning'>
-                            <b>{_t("Caution:")}</b> {_t(
+                            <b>{ _t("Caution:") }</b> { _t(
                                 "This UI does NOT check the types of the values. Use at your own risk.",
-                            )}
+                            ) }
                         </div>
 
                         <div>
-                            {_t("Setting definition:")}
-                            <pre><code>{JSON.stringify(SETTINGS[this.state.editSetting], null, 4)}</code></pre>
+                            { _t("Setting definition:") }
+                            <pre><code>{ JSON.stringify(SETTINGS[this.state.editSetting], null, 4) }</code></pre>
                         </div>
 
                         <div>
                             <table>
                                 <thead>
                                     <tr>
-                                        <th>{_t("Level")}</th>
-                                        <th>{_t("Settable at global")}</th>
-                                        <th>{_t("Settable at room")}</th>
+                                        <th>{ _t("Level") }</th>
+                                        <th>{ _t("Settable at global") }</th>
+                                        <th>{ _t("Settable at room") }</th>
                                     </tr>
                                 </thead>
                                 <tbody>
-                                    {LEVEL_ORDER.map(lvl => (
+                                    { LEVEL_ORDER.map(lvl => (
                                         <tr key={lvl}>
-                                            <td><code>{lvl}</code></td>
-                                            {this.renderCanEditLevel(null, lvl)}
-                                            {this.renderCanEditLevel(room.roomId, lvl)}
+                                            <td><code>{ lvl }</code></td>
+                                            { this.renderCanEditLevel(null, lvl) }
+                                            { this.renderCanEditLevel(room.roomId, lvl) }
                                         </tr>
-                                    ))}
+                                    )) }
                                 </tbody>
                             </table>
                         </div>
@@ -1122,8 +1122,8 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
 
                     </div>
                     <div className="mx_Dialog_buttons">
-                        <button onClick={this.onSaveClick}>{_t("Save setting values")}</button>
-                        <button onClick={this.onBack}>{_t("Back")}</button>
+                        <button onClick={this.onSaveClick}>{ _t("Save setting values") }</button>
+                        <button onClick={this.onBack}>{ _t("Back") }</button>
                     </div>
                 </div>
             );
@@ -1131,39 +1131,39 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
             return (
                 <div>
                     <div className="mx_Dialog_content mx_DevTools_SettingsExplorer">
-                        <h3>{_t("Setting:")} <code>{this.state.viewSetting}</code></h3>
+                        <h3>{ _t("Setting:") } <code>{ this.state.viewSetting }</code></h3>
 
                         <div>
-                            {_t("Setting definition:")}
-                            <pre><code>{JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}</code></pre>
+                            { _t("Setting definition:") }
+                            <pre><code>{ JSON.stringify(SETTINGS[this.state.viewSetting], null, 4) }</code></pre>
                         </div>
 
                         <div>
-                            {_t("Value:")}&nbsp;
-                            <code>{this.renderSettingValue(
+                            { _t("Value:") }&nbsp;
+                            <code>{ this.renderSettingValue(
                                 SettingsStore.getValue(this.state.viewSetting),
-                            )}</code>
+                            ) }</code>
                         </div>
 
                         <div>
-                            {_t("Value in this room:")}&nbsp;
-                            <code>{this.renderSettingValue(
+                            { _t("Value in this room:") }&nbsp;
+                            <code>{ this.renderSettingValue(
                                 SettingsStore.getValue(this.state.viewSetting, room.roomId),
-                            )}</code>
+                            ) }</code>
                         </div>
 
                         <div>
-                            {_t("Values at explicit levels:")}
-                            <pre><code>{this.renderExplicitSettingValues(
+                            { _t("Values at explicit levels:") }
+                            <pre><code>{ this.renderExplicitSettingValues(
                                 this.state.viewSetting, null,
-                            )}</code></pre>
+                            ) }</code></pre>
                         </div>
 
                         <div>
-                            {_t("Values at explicit levels in this room:")}
-                            <pre><code>{this.renderExplicitSettingValues(
+                            { _t("Values at explicit levels in this room:") }
+                            <pre><code>{ this.renderExplicitSettingValues(
                                 this.state.viewSetting, room.roomId,
-                            )}</code></pre>
+                            ) }</code></pre>
                         </div>
 
                     </div>
@@ -1171,7 +1171,7 @@ class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExpl
                         <button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{
                             _t("Edit Values")
                         }</button>
-                        <button onClick={this.onBack}>{_t("Back")}</button>
+                        <button onClick={this.onBack}>{ _t("Back") }</button>
                     </div>
                 </div>
             );
@@ -1232,12 +1232,12 @@ export default class DevtoolsDialog extends React.PureComponent<IProps, IState>
 
         if (this.state.mode) {
             body = <MatrixClientContext.Consumer>
-                {(cli) => <React.Fragment>
+                { (cli) => <React.Fragment>
                     <div className="mx_DevTools_label_left">{ this.state.mode.getLabel() }</div>
                     <div className="mx_DevTools_label_right">Room ID: { this.props.roomId }</div>
                     <div className="mx_DevTools_label_bottom" />
                     <this.state.mode onBack={this.onBack} room={cli.getRoom(this.props.roomId)} />
-                </React.Fragment>}
+                </React.Fragment> }
             </MatrixClientContext.Consumer>;
         } else {
             const classes = "mx_DevTools_RoomStateExplorer_button";
diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
index 217e4f2d37..1eabb68081 100644
--- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
+++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
@@ -151,16 +151,16 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
                             <AccessibleButton
                                 onClick={this.onChangeAvatar}
                                 className="mx_EditCommunityPrototypeDialog_avatarContainer"
-                            >{preview}</AccessibleButton>
+                            >{ preview }</AccessibleButton>
                             <div className="mx_EditCommunityPrototypeDialog_tip">
-                                <b>{_t("Add image (optional)")}</b>
+                                <b>{ _t("Add image (optional)") }</b>
                                 <span>
-                                    {_t("An image will help people identify your community.")}
+                                    { _t("An image will help people identify your community.") }
                                 </span>
                             </div>
                         </div>
                         <AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
-                            {_t("Save")}
+                            { _t("Save") }
                         </AccessibleButton>
                     </div>
                 </form>
diff --git a/src/components/views/dialogs/FeedbackDialog.js b/src/components/views/dialogs/FeedbackDialog.js
index 88a57cf8cb..85171c9bf6 100644
--- a/src/components/views/dialogs/FeedbackDialog.js
+++ b/src/components/views/dialogs/FeedbackDialog.js
@@ -58,10 +58,10 @@ export default (props) => {
         countlyFeedbackSection = <React.Fragment>
             <hr />
             <div className="mx_FeedbackDialog_section mx_FeedbackDialog_rateApp">
-                <h3>{_t("Rate %(brand)s", { brand })}</h3>
+                <h3>{ _t("Rate %(brand)s", { brand }) }</h3>
 
-                <p>{_t("Tell us below how you feel about %(brand)s so far.", { brand })}</p>
-                <p>{_t("Please go into as much detail as you like, so we can track down the problem.")}</p>
+                <p>{ _t("Tell us below how you feel about %(brand)s so far.", { brand }) }</p>
+                <p>{ _t("Please go into as much detail as you like, so we can track down the problem.") }</p>
 
                 <StyledRadioGroup
                     name="feedbackRating"
@@ -95,7 +95,7 @@ export default (props) => {
     let subheading;
     if (hasFeedback) {
         subheading = (
-            <h2>{_t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand })}</h2>
+            <h2>{ _t("There are two ways you can provide feedback and help us improve %(brand)s.", { brand }) }</h2>
         );
     }
 
@@ -106,7 +106,7 @@ export default (props) => {
                 _t("PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> " +
                     "to help us track down the problem.", {}, {
                     debugLogsLink: sub => (
-                        <AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{sub}</AccessibleButton>
+                        <AccessibleButton kind="link" onClick={onDebugLogsLinkClick}>{ sub }</AccessibleButton>
                     ),
                 })
             }</p>
@@ -121,7 +121,7 @@ export default (props) => {
             { subheading }
 
             <div className="mx_FeedbackDialog_section mx_FeedbackDialog_reportBug">
-                <h3>{_t("Report a bug")}</h3>
+                <h3>{ _t("Report a bug") }</h3>
                 <p>{
                     _t("Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. " +
                         "No match? <newIssueLink>Start a new one</newIssueLink>.", {}, {
@@ -133,7 +133,7 @@ export default (props) => {
                         },
                     })
                 }</p>
-                {bugReports}
+                { bugReports }
             </div>
             { countlyFeedbackSection }
         </React.Fragment>}
diff --git a/src/components/views/dialogs/HostSignupDialog.tsx b/src/components/views/dialogs/HostSignupDialog.tsx
index 64c080bf01..4b8b7f32f0 100644
--- a/src/components/views/dialogs/HostSignupDialog.tsx
+++ b/src/components/views/dialogs/HostSignupDialog.tsx
@@ -177,32 +177,32 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
         const textComponent = (
             <>
                 <p>
-                    {_t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
+                    { _t("Continuing temporarily allows the %(hostSignupBrand)s setup process to access your " +
                         "account to fetch verified email addresses. This data is not stored.", {
                         hostSignupBrand: this.config.brand,
-                    })}
+                    }) }
                 </p>
                 <p>
-                    {_t("Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.",
+                    { _t("Learn more in our <privacyPolicyLink />, <termsOfServiceLink /> and <cookiePolicyLink />.",
                         {},
                         {
                             cookiePolicyLink: () => (
                                 <a href={this.config.cookiePolicyUrl} target="_blank" rel="noreferrer noopener">
-                                    {_t("Cookie Policy")}
+                                    { _t("Cookie Policy") }
                                 </a>
                             ),
                             privacyPolicyLink: () => (
                                 <a href={this.config.privacyPolicyUrl} target="_blank" rel="noreferrer noopener">
-                                    {_t("Privacy Policy")}
+                                    { _t("Privacy Policy") }
                                 </a>
                             ),
                             termsOfServiceLink: () => (
                                 <a href={this.config.termsOfServiceUrl} target="_blank" rel="noreferrer noopener">
-                                    {_t("Terms of Service")}
+                                    { _t("Terms of Service") }
                                 </a>
                             ),
                         },
-                    )}
+                    ) }
                 </p>
             </>
         );
@@ -241,12 +241,12 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
                                 },
                             )}
                         >
-                            {this.state.minimized &&
+                            { this.state.minimized &&
                                 <div className="mx_Dialog_header mx_Dialog_headerWithButton">
                                     <div className="mx_Dialog_title">
-                                        {_t("%(hostSignupBrand)s Setup", {
+                                        { _t("%(hostSignupBrand)s Setup", {
                                             hostSignupBrand: this.config.brand,
-                                        })}
+                                        }) }
                                     </div>
                                     <AccessibleButton
                                         className="mx_HostSignup_maximize_button"
@@ -256,7 +256,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
                                     />
                                 </div>
                             }
-                            {!this.state.minimized &&
+                            { !this.state.minimized &&
                                 <div className="mx_Dialog_header mx_Dialog_headerWithCancel">
                                     <AccessibleButton
                                         onClick={this.minimizeDialog}
@@ -272,12 +272,12 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
                                     />
                                 </div>
                             }
-                            {this.state.error &&
+                            { this.state.error &&
                                 <div>
-                                    {this.state.error}
+                                    { this.state.error }
                                 </div>
                             }
-                            {!this.state.error &&
+                            { !this.state.error &&
                                 <iframe
                                     src={this.config.url}
                                     ref={this.iframeRef}
diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js
index d919b61d38..963d98ef3f 100644
--- a/src/components/views/dialogs/IncomingSasDialog.js
+++ b/src/components/views/dialogs/IncomingSasDialog.js
@@ -138,7 +138,7 @@ export default class IncomingSasDialog extends React.Component {
                     url={url}
                     width={48} height={48} resizeMethod='crop'
                 />
-                <h2>{oppProfile.displayname}</h2>
+                <h2>{ oppProfile.displayname }</h2>
             </div>;
         } else if (this.state.opponentProfileError) {
             profile = <div>
@@ -146,42 +146,42 @@ export default class IncomingSasDialog extends React.Component {
                     idName={this.props.verifier.userId}
                     width={48} height={48}
                 />
-                <h2>{this.props.verifier.userId}</h2>
+                <h2>{ this.props.verifier.userId }</h2>
             </div>;
         } else {
             profile = <Spinner />;
         }
 
         const userDetailText = [
-            <p key="p1">{_t(
+            <p key="p1">{ _t(
                 "Verify this user to mark them as trusted. " +
                 "Trusting users gives you extra peace of mind when using " +
                 "end-to-end encrypted messages.",
-            )}</p>,
-            <p key="p2">{_t(
+            ) }</p>,
+            <p key="p2">{ _t(
                 // NB. Below wording adjusted to singular 'session' until we have
                 // cross-signing
                 "Verifying this user will mark their session as trusted, and " +
                 "also mark your session as trusted to them.",
-            )}</p>,
+            ) }</p>,
         ];
 
         const selfDetailText = [
-            <p key="p1">{_t(
+            <p key="p1">{ _t(
                 "Verify this device to mark it as trusted. " +
                 "Trusting this device gives you and other users extra peace of mind when using " +
                 "end-to-end encrypted messages.",
-            )}</p>,
-            <p key="p2">{_t(
+            ) }</p>,
+            <p key="p2">{ _t(
                 "Verifying this device will mark it as trusted, and users who have verified with " +
                 "you will trust this device.",
-            )}</p>,
+            ) }</p>,
         ];
 
         return (
             <div>
-                {profile}
-                {isSelf ? selfDetailText : userDetailText}
+                { profile }
+                { isSelf ? selfDetailText : userDetailText }
                 <DialogButtons
                     primaryButton={_t('Continue')}
                     hasCancel={true}
@@ -209,7 +209,7 @@ export default class IncomingSasDialog extends React.Component {
         return (
             <div>
                 <Spinner />
-                <p>{_t("Waiting for partner to confirm...")}</p>
+                <p>{ _t("Waiting for partner to confirm...") }</p>
             </div>
         );
     }
@@ -251,7 +251,7 @@ export default class IncomingSasDialog extends React.Component {
                 onFinished={this._onFinished}
                 fixedWidth={false}
             >
-                {body}
+                { body }
             </BaseDialog>
         );
     }
diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.js b/src/components/views/dialogs/IntegrationsDisabledDialog.js
index 1e2ff09196..6a5b2f08f9 100644
--- a/src/components/views/dialogs/IntegrationsDisabledDialog.js
+++ b/src/components/views/dialogs/IntegrationsDisabledDialog.js
@@ -49,7 +49,7 @@ export default class IntegrationsDisabledDialog extends React.Component {
                 title={_t("Integrations are disabled")}
             >
                 <div className='mx_IntegrationsDisabledDialog_content'>
-                    <p>{_t("Enable 'Manage Integrations' in Settings to do this.")}</p>
+                    <p>{ _t("Enable 'Manage Integrations' in Settings to do this.") }</p>
                 </div>
                 <DialogButtons
                     primaryButton={_t("Settings")}
diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js
index 30b6904f27..6cfb96a1b4 100644
--- a/src/components/views/dialogs/IntegrationsImpossibleDialog.js
+++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js
@@ -45,11 +45,11 @@ export default class IntegrationsImpossibleDialog extends React.Component {
             >
                 <div className='mx_IntegrationsImpossibleDialog_content'>
                     <p>
-                        {_t(
+                        { _t(
                             "Your %(brand)s doesn't allow you to use an integration manager to do this. " +
                             "Please contact an admin.",
                             { brand },
-                        )}
+                        ) }
                     </p>
                 </div>
                 <DialogButtons
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js
index 09da72cab0..e5f4887f06 100644
--- a/src/components/views/dialogs/InteractiveAuthDialog.js
+++ b/src/components/views/dialogs/InteractiveAuthDialog.js
@@ -163,7 +163,7 @@ export default class InteractiveAuthDialog extends React.Component {
         } else {
             content = (
                 <div id='mx_Dialog_content'>
-                    {body}
+                    { body }
                     <InteractiveAuth
                         ref={this._collectInteractiveAuth}
                         matrixClient={this.props.matrixClient}
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 58bab511bf..46f140fd39 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -224,8 +224,8 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
         return (
             <span className='mx_InviteDialog_userTile'>
                 <span className='mx_InviteDialog_userTile_pill'>
-                    {avatar}
-                    <span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
+                    { avatar }
+                    <span className='mx_InviteDialog_userTile_name'>{ this.props.member.name }</span>
                 </span>
                 { closeButton }
             </span>
@@ -267,20 +267,20 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
             // Push any text we missed (first bit/middle of text)
             if (ii > i) {
                 // Push any text we aren't highlighting (middle of text match, or beginning of text)
-                result.push(<span key={i + 'begin'}>{str.substring(i, ii)}</span>);
+                result.push(<span key={i + 'begin'}>{ str.substring(i, ii) }</span>);
             }
 
             i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching)
 
             // Highlight the word the user entered
             const substr = str.substring(i, filterStr.length + i);
-            result.push(<span className='mx_InviteDialog_roomTile_highlight' key={i + 'bold'}>{substr}</span>);
+            result.push(<span className='mx_InviteDialog_roomTile_highlight' key={i + 'bold'}>{ substr }</span>);
             i += substr.length;
         }
 
         // Push any text we missed (end of text)
         if (i < str.length) {
-            result.push(<span key={i + 'end'}>{str.substring(i)}</span>);
+            result.push(<span key={i + 'end'}>{ str.substring(i) }</span>);
         }
 
         return result;
@@ -290,7 +290,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
         let timestamp = null;
         if (this.props.lastActiveTs) {
             const humanTs = humanizeTime(this.props.lastActiveTs);
-            timestamp = <span className='mx_InviteDialog_roomTile_time'>{humanTs}</span>;
+            timestamp = <span className='mx_InviteDialog_roomTile_time'>{ humanTs }</span>;
         }
 
         const avatarSize = 36;
@@ -317,8 +317,8 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
         // the browser from reloading the image source when the avatar remounts).
         const stackedAvatar = (
             <span className='mx_InviteDialog_roomTile_avatarStack'>
-                {avatar}
-                {checkmark}
+                { avatar }
+                { checkmark }
             </span>
         );
 
@@ -328,12 +328,12 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
 
         return (
             <div className='mx_InviteDialog_roomTile' onClick={this.onClick}>
-                {stackedAvatar}
+                { stackedAvatar }
                 <span className="mx_InviteDialog_roomTile_nameStack">
-                    <div className='mx_InviteDialog_roomTile_name'>{this.highlightName(this.props.member.name)}</div>
-                    <div className='mx_InviteDialog_roomTile_userId'>{caption}</div>
+                    <div className='mx_InviteDialog_roomTile_name'>{ this.highlightName(this.props.member.name) }</div>
+                    <div className='mx_InviteDialog_roomTile_userId'>{ caption }</div>
                 </span>
-                {timestamp}
+                { timestamp }
             </div>
         );
     }
@@ -1152,8 +1152,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             if (sourceMembers.length === 0 && !hasAdditionalMembers) {
                 return (
                     <div className='mx_InviteDialog_section'>
-                        <h3>{sectionName}</h3>
-                        <p>{_t("No results")}</p>
+                        <h3>{ sectionName }</h3>
+                        <p>{ _t("No results") }</p>
                     </div>
                 );
             }
@@ -1175,7 +1175,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         if (hasMore) {
             showMore = (
                 <AccessibleButton onClick={showMoreFn} kind="link">
-                    {_t("Show more")}
+                    { _t("Show more") }
                 </AccessibleButton>
             );
         }
@@ -1192,10 +1192,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         ));
         return (
             <div className='mx_InviteDialog_section'>
-                <h3>{sectionName}</h3>
-                {sectionSubname ? <p className="mx_InviteDialog_subname">{sectionSubname}</p> : null}
-                {tiles}
-                {showMore}
+                <h3>{ sectionName }</h3>
+                { sectionSubname ? <p className="mx_InviteDialog_subname">{ sectionSubname }</p> : null }
+                { tiles }
+                { showMore }
             </div>
         );
     }
@@ -1225,8 +1225,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         );
         return (
             <div className='mx_InviteDialog_editor' onClick={this.onClickInputArea}>
-                {targets}
-                {input}
+                { targets }
+                { input }
             </div>
         );
     }
@@ -1241,7 +1241,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
         if (defaultIdentityServerUrl) {
             return (
-                <div className="mx_AddressPickerDialog_identityServer">{_t(
+                <div className="mx_AddressPickerDialog_identityServer">{ _t(
                     "Use an identity server to invite by email. " +
                     "<default>Use the default (%(defaultIdentityServerName)s)</default> " +
                     "or manage in <settings>Settings</settings>.",
@@ -1249,20 +1249,20 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                         defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
                     },
                     {
-                        default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
-                        settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
+                        default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{ sub }</a>,
+                        settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
                     },
-                )}</div>
+                ) }</div>
             );
         } else {
             return (
-                <div className="mx_AddressPickerDialog_identityServer">{_t(
+                <div className="mx_AddressPickerDialog_identityServer">{ _t(
                     "Use an identity server to invite by email. " +
                     "Manage in <settings>Settings</settings>.",
                     {}, {
-                        settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
+                        settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
                     },
-                )}</div>
+                ) }</div>
             );
         }
     }
@@ -1339,7 +1339,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                     {},
                     { userId: () => {
                         return (
-                            <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
+                            <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>
                         );
                     } },
                 );
@@ -1349,7 +1349,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                     {},
                     { userId: () => {
                         return (
-                            <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
+                            <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>
                         );
                     } },
                 );
@@ -1367,7 +1367,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                                     href={makeUserPermalink(userId)}
                                     rel="noreferrer noopener"
                                     target="_blank"
-                                >{userId}</a>
+                                >{ userId }</a>
                             );
                         },
                         a: (sub) => {
@@ -1375,13 +1375,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                                 <AccessibleButton
                                     kind="link"
                                     onClick={this.onCommunityInviteClick}
-                                >{sub}</AccessibleButton>
+                                >{ sub }</AccessibleButton>
                             );
                         },
                     },
                 );
                 helpText = <React.Fragment>
-                    { helpText } {inviteText}
+                    { helpText } { inviteText }
                 </React.Fragment>;
             }
             buttonText = _t("Go");
@@ -1438,9 +1438,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 
             helpText = _t(helpTextUntranslated, {}, {
                 userId: () =>
-                    <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
+                    <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>,
                 a: (sub) =>
-                    <a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
+                    <a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{ sub }</a>,
             });
 
             buttonText = _t("Invite");
@@ -1459,7 +1459,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                             <img
                                 src={require("../../../../res/img/element-icons/info.svg")}
                                 width={14} height={14} />
-                            {" " + _t("Invited people will be able to read old messages.")}
+                            { " " + _t("Invited people will be able to read old messages.") }
                         </p>;
                 }
             }
@@ -1469,14 +1469,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             consultConnectSection = <div className="mx_InviteDialog_transferConsultConnect">
                 <label>
                     <input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
-                    {_t("Consult first")}
+                    { _t("Consult first") }
                 </label>
                 <AccessibleButton
                     kind="secondary"
                     onClick={this.onCancel}
                     className='mx_InviteDialog_transferConsultConnect_pushRight'
                 >
-                    {_t("Cancel")}
+                    { _t("Cancel") }
                 </AccessibleButton>
                 <AccessibleButton
                     kind="primary"
@@ -1484,7 +1484,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                     className='mx_InviteDialog_transferButton'
                     disabled={!hasSelection && this.state.dialPadValue === ''}
                 >
-                    {_t("Transfer")}
+                    { _t("Transfer") }
                 </AccessibleButton>
             </div>;
         } else {
@@ -1497,27 +1497,27 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             className='mx_InviteDialog_goButton'
             disabled={this.state.busy || !hasSelection}
         >
-            {buttonText}
+            { buttonText }
         </AccessibleButton>;
 
         const usersSection = <React.Fragment>
-            <p className='mx_InviteDialog_helpText'>{helpText}</p>
+            <p className='mx_InviteDialog_helpText'>{ helpText }</p>
             <div className='mx_InviteDialog_addressBar'>
-                {this.renderEditor()}
+                { this.renderEditor() }
                 <div className='mx_InviteDialog_buttonAndSpinner'>
-                    {goButton}
-                    {spinner}
+                    { goButton }
+                    { spinner }
                 </div>
             </div>
-            {keySharingWarning}
-            {this.renderIdentityServerWarning()}
-            <div className='error'>{this.state.errorText}</div>
+            { keySharingWarning }
+            { this.renderIdentityServerWarning() }
+            <div className='error'>{ this.state.errorText }</div>
             <div className='mx_InviteDialog_userSections'>
-                {this.renderSection('recents')}
-                {this.renderSection('suggestions')}
-                {extraSection}
+                { this.renderSection('recents') }
+                { this.renderSection('suggestions') }
+                { extraSection }
             </div>
-            {footer}
+            { footer }
         </React.Fragment>;
 
         let dialogContent;
@@ -1550,7 +1550,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 
             const dialPadSection = <div className="mx_InviteDialog_dialPad">
                 <form onSubmit={this.onDialFormSubmit}>
-                    {dialPadField}
+                    { dialPadField }
                 </form>
                 <Dialpad hasDial={false}
                     onDigitPress={this.onDigitPress} onDeletePress={this.onDeletePress}
@@ -1561,12 +1561,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                 <TabbedView tabs={tabs} initialTabId={this.state.currentTabId}
                     tabLocation={TabLocation.TOP} onChange={this.onTabChange}
                 />
-                {consultConnectSection}
+                { consultConnectSection }
             </React.Fragment>;
         } else {
             dialogContent = <React.Fragment>
-                {usersSection}
-                {consultConnectSection}
+                { usersSection }
+                { consultConnectSection }
             </React.Fragment>;
         }
 
@@ -1582,7 +1582,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                 title={title}
             >
                 <div className='mx_InviteDialog_content'>
-                    {dialogContent}
+                    { dialogContent }
                 </div>
             </BaseDialog>
         );
diff --git a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js
index 22487af17c..9fa9fc1373 100644
--- a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js
+++ b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js
@@ -69,10 +69,10 @@ export default function KeySignatureUploadFailedDialog({
         const brand = SdkConfig.get().brand;
 
         body = (<div>
-            <p>{_t("%(brand)s encountered an error during upload of:", { brand })}</p>
-            <p>{reason}</p>
-            {retrying && <Spinner />}
-            <pre>{JSON.stringify(failures, null, 2)}</pre>
+            <p>{ _t("%(brand)s encountered an error during upload of:", { brand }) }</p>
+            <p>{ reason }</p>
+            { retrying && <Spinner /> }
+            <pre>{ JSON.stringify(failures, null, 2) }</pre>
             <DialogButtons
                 primaryButton='Retry'
                 hasCancel={true}
@@ -83,11 +83,11 @@ export default function KeySignatureUploadFailedDialog({
         </div>);
     } else {
         body = (<div>
-            {success ?
-                <span>{_t("Upload completed")}</span> :
+            { success ?
+                <span>{ _t("Upload completed") }</span> :
                 cancelled ?
-                    <span>{_t("Cancelled signature upload")}</span> :
-                    <span>{_t("Unable to upload")}</span>}
+                    <span>{ _t("Cancelled signature upload") }</span> :
+                    <span>{ _t("Unable to upload") }</span> }
             <DialogButtons
                 primaryButton={_t("OK")}
                 hasCancel={false}
@@ -104,7 +104,7 @@ export default function KeySignatureUploadFailedDialog({
             fixedWidth={false}
             onFinished={() => {}}
         >
-            {body}
+            { body }
         </BaseDialog>
     );
 }
diff --git a/src/components/views/dialogs/LazyLoadingDisabledDialog.js b/src/components/views/dialogs/LazyLoadingDisabledDialog.js
index cae9510742..e43cb28a22 100644
--- a/src/components/views/dialogs/LazyLoadingDisabledDialog.js
+++ b/src/components/views/dialogs/LazyLoadingDisabledDialog.js
@@ -44,7 +44,7 @@ export default (props) => {
     return (<QuestionDialog
         hasCancelButton={false}
         title={_t("Incompatible local cache")}
-        description={<div><p>{description1}</p><p>{description2}</p></div>}
+        description={<div><p>{ description1 }</p><p>{ description2 }</p></div>}
         button={_t("Clear cache and resync")}
         onFinished={props.onFinished}
     />);
diff --git a/src/components/views/dialogs/LazyLoadingResyncDialog.js b/src/components/views/dialogs/LazyLoadingResyncDialog.js
index 378306dc2f..a5db15ebbe 100644
--- a/src/components/views/dialogs/LazyLoadingResyncDialog.js
+++ b/src/components/views/dialogs/LazyLoadingResyncDialog.js
@@ -33,7 +33,7 @@ export default (props) => {
     return (<QuestionDialog
         hasCancelButton={false}
         title={_t("Updating %(brand)s", { brand })}
-        description={<div>{description}</div>}
+        description={<div>{ description }</div>}
         button={_t("OK")}
         onFinished={props.onFinished}
     />);
diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js
index bd9411358b..469cd48093 100644
--- a/src/components/views/dialogs/LogoutDialog.js
+++ b/src/components/views/dialogs/LogoutDialog.js
@@ -123,11 +123,11 @@ export default class LogoutDialog extends React.Component {
             const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
 
             const description = <div>
-                <p>{_t(
+                <p>{ _t(
                     "Encrypted messages are secured with end-to-end encryption. " +
                     "Only you and the recipient(s) have the keys to read these messages.",
-                )}</p>
-                <p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
+                ) }</p>
+                <p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
             </div>;
 
             let dialogContent;
@@ -156,13 +156,13 @@ export default class LogoutDialog extends React.Component {
                         focus={true}
                     >
                         <button onClick={this._onLogoutConfirm}>
-                            {_t("I don't want my encrypted messages")}
+                            { _t("I don't want my encrypted messages") }
                         </button>
                     </DialogButtons>
                     <details>
-                        <summary>{_t("Advanced")}</summary>
+                        <summary>{ _t("Advanced") }</summary>
                         <p><button onClick={this._onExportE2eKeysClicked}>
-                            {_t("Manually export keys")}
+                            { _t("Manually export keys") }
                         </button></p>
                     </details>
                 </div>;
@@ -176,7 +176,7 @@ export default class LogoutDialog extends React.Component {
                 hasCancel={true}
                 onFinished={this._onFinished}
             >
-                {dialogContent}
+                { dialogContent }
             </BaseDialog>);
         } else {
             const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.js b/src/components/views/dialogs/MessageEditHistoryDialog.js
index b9225f5932..6fce8aecd4 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.js
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.js
@@ -134,18 +134,18 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
             const { error } = this.state;
             if (error.errcode === "M_UNRECOGNIZED") {
                 content = (<p className="mx_MessageEditHistoryDialog_error">
-                    {_t("Your homeserver doesn't seem to support this feature.")}
+                    { _t("Your homeserver doesn't seem to support this feature.") }
                 </p>);
             } else if (error.errcode) {
                 // some kind of error from the homeserver
                 content = (<p className="mx_MessageEditHistoryDialog_error">
-                    {_t("Something went wrong!")}
+                    { _t("Something went wrong!") }
                 </p>);
             } else {
                 content = (<p className="mx_MessageEditHistoryDialog_error">
-                    {_t("Cannot reach homeserver")}
+                    { _t("Cannot reach homeserver") }
                     <br />
-                    {_t("Ensure you have a stable internet connection, or get in touch with the server admin")}
+                    { _t("Ensure you have a stable internet connection, or get in touch with the server admin") }
                 </p>);
             }
         } else if (this.state.isLoading) {
@@ -155,11 +155,11 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
             const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
             content = (<ScrollPanel
                 className="mx_MessageEditHistoryDialog_scrollPanel"
-                onFillRequest={ this.loadMoreEdits }
+                onFillRequest={this.loadMoreEdits}
                 stickyBottom={false}
                 startAtBottom={false}
             >
-                <ul className="mx_MessageEditHistoryDialog_edits">{this._renderEdits()}</ul>
+                <ul className="mx_MessageEditHistoryDialog_edits">{ this._renderEdits() }</ul>
             </ScrollPanel>);
         }
         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@@ -170,7 +170,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
                 onFinished={this.props.onFinished}
                 title={_t("Message edits")}
             >
-                {content}
+                { content }
             </BaseDialog>
         );
     }
diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx
index 6bc84b66b4..1bf7eb7307 100644
--- a/src/components/views/dialogs/ModalWidgetDialog.tsx
+++ b/src/components/views/dialogs/ModalWidgetDialog.tsx
@@ -191,9 +191,9 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
                     width="16"
                     alt=""
                 />
-                {_t("Data on this screen is shared with %(widgetDomain)s", {
+                { _t("Data on this screen is shared with %(widgetDomain)s", {
                     widgetDomain: parsed.hostname,
-                })}
+                }) }
             </div>
             <div>
                 <iframe
diff --git a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx
index 98f1a79b4f..804a1aec35 100644
--- a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx
+++ b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx
@@ -67,10 +67,10 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
         fixedWidth={false}
     >
         <div className="mx_Dialog_content" id="mx_RegistrationEmailPromptDialog">
-            <p>{_t("Just a heads up, if you don't add an email and forget your password, you could " +
+            <p>{ _t("Just a heads up, if you don't add an email and forget your password, you could " +
                 "<b>permanently lose access to your account</b>.", {}, {
-                b: sub => <b>{sub}</b>,
-            })}</p>
+                b: sub => <b>{ sub }</b>,
+            }) }</p>
             <form onSubmit={onSubmit}>
                 <Field
                     ref={fieldRef}
diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx
index 494fd59082..25675ee7d2 100644
--- a/src/components/views/dialogs/ReportEventDialog.tsx
+++ b/src/components/views/dialogs/ReportEventDialog.tsx
@@ -40,7 +40,7 @@ interface IState {
     busy: boolean;
     err?: string;
     // If we know it, the nature of the abuse, as specified by MSC3215.
-    nature?: EXTENDED_NATURE;
+    nature?: ExtendedNature;
 }
 
 const MODERATED_BY_STATE_EVENT_TYPE = [
@@ -55,22 +55,22 @@ const MODERATED_BY_STATE_EVENT_TYPE = [
 const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
 
 // Standard abuse natures.
-enum NATURE {
-    DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement",
-    TOXIC = "org.matrix.msc3215.abuse.nature.toxic",
-    ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal",
-    SPAM = "org.matrix.msc3215.abuse.nature.spam",
-    OTHER = "org.matrix.msc3215.abuse.nature.other",
+enum Nature {
+    Disagreement = "org.matrix.msc3215.abuse.nature.disagreement",
+    Toxic = "org.matrix.msc3215.abuse.nature.toxic",
+    Illegal = "org.matrix.msc3215.abuse.nature.illegal",
+    Spam = "org.matrix.msc3215.abuse.nature.spam",
+    Other = "org.matrix.msc3215.abuse.nature.other",
 }
 
-enum NON_STANDARD_NATURE {
+enum NonStandardValue {
     // Non-standard abuse nature.
     // It should never leave the client - we use it to fallback to
     // server-wide abuse reporting.
-    ADMIN = "non-standard.abuse.nature.admin"
+    Admin = "non-standard.abuse.nature.admin"
 }
 
-type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
+type ExtendedNature = Nature | NonStandardValue;
 
 type Moderation = {
     // The id of the moderation room.
@@ -170,7 +170,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
 
     // The user has clicked on a nature.
     private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
-        this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE });
+        this.setState({ nature: e.currentTarget.value as ExtendedNature });
     };
 
     // The user has clicked "cancel".
@@ -187,7 +187,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
             // We need a nature.
             // If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
             if (!this.state.nature ||
-                    ((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN)
+                    ((this.state.nature == Nature.Other || this.state.nature == NonStandardValue.Admin)
                         && !reason)
             ) {
                 this.setState({
@@ -214,8 +214,8 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
         try {
             const client = MatrixClientPeg.get();
             const ev = this.props.mxEvent;
-            if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) {
-                const nature: NATURE = this.state.nature;
+            if (this.moderation && this.state.nature != NonStandardValue.Admin) {
+                const nature: Nature = this.state.nature;
 
                 // Report to moderators through to the dedicated bot,
                 // as configured in the room's state events.
@@ -245,7 +245,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
         let error = null;
         if (this.state.err) {
             error = <div className="error">
-                {this.state.err}
+                { this.state.err }
             </div>;
         }
 
@@ -274,27 +274,27 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
             const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
             let subtitle;
             switch (this.state.nature) {
-                case NATURE.DISAGREEMENT:
+                case Nature.Disagreement:
                     subtitle = _t("What this user is writing is wrong.\n" +
                         "This will be reported to the room moderators.");
                     break;
-                case NATURE.TOXIC:
+                case Nature.Toxic:
                     subtitle = _t("This user is displaying toxic behaviour, " +
                         "for instance by insulting other users or sharing " +
                         " adult-only content in a family-friendly room " +
                         " or otherwise violating the rules of this room.\n" +
                         "This will be reported to the room moderators.");
                     break;
-                case NATURE.ILLEGAL:
+                case Nature.Illegal:
                     subtitle = _t("This user is displaying illegal behaviour, " +
                         "for instance by doxing people or threatening violence.\n" +
                         "This will be reported to the room moderators who may escalate this to legal authorities.");
                     break;
-                case NATURE.SPAM:
+                case Nature.Spam:
                     subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
                         "This will be reported to the room moderators.");
                     break;
-                case NON_STANDARD_NATURE.ADMIN:
+                case NonStandardValue.Admin:
                     if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
                         subtitle = _t("This room is dedicated to illegal or toxic content " +
                             "or the moderators fail to moderate illegal or toxic content.\n" +
@@ -308,7 +308,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
                         { homeserver: homeServerName });
                     }
                     break;
-                case NATURE.OTHER:
+                case Nature.Other:
                     subtitle = _t("Any other reason. Please describe the problem.\n" +
                         "This will be reported to the room moderators.");
                     break;
@@ -326,55 +326,55 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
                 >
                     <div>
                         <StyledRadioButton
-                            name = "nature"
-                            value = { NATURE.DISAGREEMENT }
-                            checked = { this.state.nature == NATURE.DISAGREEMENT }
-                            onChange = { this.onNatureChosen }
+                            name="nature"
+                            value={Nature.Disagreement}
+                            checked={this.state.nature == Nature.Disagreement}
+                            onChange={this.onNatureChosen}
                         >
-                            {_t('Disagree')}
+                            { _t('Disagree') }
                         </StyledRadioButton>
                         <StyledRadioButton
-                            name = "nature"
-                            value = { NATURE.TOXIC }
-                            checked = { this.state.nature == NATURE.TOXIC }
-                            onChange = { this.onNatureChosen }
+                            name="nature"
+                            value={Nature.Toxic}
+                            checked={this.state.nature == Nature.Toxic}
+                            onChange={this.onNatureChosen}
                         >
-                            {_t('Toxic Behaviour')}
+                            { _t('Toxic Behaviour') }
                         </StyledRadioButton>
                         <StyledRadioButton
-                            name = "nature"
-                            value = { NATURE.ILLEGAL }
-                            checked = { this.state.nature == NATURE.ILLEGAL }
-                            onChange = { this.onNatureChosen }
+                            name="nature"
+                            value={Nature.Illegal}
+                            checked={this.state.nature == Nature.Illegal}
+                            onChange={this.onNatureChosen}
                         >
-                            {_t('Illegal Content')}
+                            { _t('Illegal Content') }
                         </StyledRadioButton>
                         <StyledRadioButton
-                            name = "nature"
-                            value = { NATURE.SPAM }
-                            checked = { this.state.nature == NATURE.SPAM }
-                            onChange = { this.onNatureChosen }
+                            name="nature"
+                            value={Nature.Spam}
+                            checked={this.state.nature == Nature.Spam}
+                            onChange={this.onNatureChosen}
                         >
-                            {_t('Spam or propaganda')}
+                            { _t('Spam or propaganda') }
                         </StyledRadioButton>
                         <StyledRadioButton
-                            name = "nature"
-                            value = { NON_STANDARD_NATURE.ADMIN }
-                            checked = { this.state.nature == NON_STANDARD_NATURE.ADMIN }
-                            onChange = { this.onNatureChosen }
+                            name="nature"
+                            value={NonStandardValue.Admin}
+                            checked={this.state.nature == NonStandardValue.Admin}
+                            onChange={this.onNatureChosen}
                         >
-                            {_t('Report the entire room')}
+                            { _t('Report the entire room') }
                         </StyledRadioButton>
                         <StyledRadioButton
-                            name = "nature"
-                            value = { NATURE.OTHER }
-                            checked = { this.state.nature == NATURE.OTHER }
-                            onChange = { this.onNatureChosen }
+                            name="nature"
+                            value={Nature.Other}
+                            checked={this.state.nature == Nature.Other}
+                            onChange={this.onNatureChosen}
                         >
-                            {_t('Other')}
+                            { _t('Other') }
                         </StyledRadioButton>
                         <p>
-                            {subtitle}
+                            { subtitle }
                         </p>
                         <Field
                             className="mx_ReportEventDialog_reason"
@@ -385,8 +385,8 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
                             value={this.state.reason}
                             disabled={this.state.busy}
                         />
-                        {progress}
-                        {error}
+                        { progress }
+                        { error }
                     </div>
                     <DialogButtons
                         primaryButton={_t("Send report")}
@@ -416,7 +416,7 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
                                 "or images.")
                         }
                     </p>
-                    {adminMessage}
+                    { adminMessage }
                     <Field
                         className="mx_ReportEventDialog_reason"
                         element="textarea"
@@ -426,8 +426,8 @@ export default class ReportEventDialog extends React.Component<IProps, IState> {
                         value={this.state.reason}
                         disabled={this.state.busy}
                     />
-                    {progress}
-                    {error}
+                    { progress }
+                    { error }
                 </div>
                 <DialogButtons
                     primaryButton={_t("Send report")}
diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js
index 90092df7a5..e48f728383 100644
--- a/src/components/views/dialogs/RoomUpgradeDialog.js
+++ b/src/components/views/dialogs/RoomUpgradeDialog.js
@@ -88,19 +88,19 @@ export default class RoomUpgradeDialog extends React.Component {
                 hasCancel={true}
             >
                 <p>
-                    {_t(
+                    { _t(
                         "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:",
-                    )}
+                    ) }
                 </p>
                 <ol>
-                    <li>{_t("Create a new room with the same name, description and avatar")}</li>
-                    <li>{_t("Update any local room aliases to point to the new room")}</li>
-                    <li>{_t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room")}</li>
-                    <li>{_t("Put a link back to the old room at the start of the new room so people can see old messages")}</li>
+                    <li>{ _t("Create a new room with the same name, description and avatar") }</li>
+                    <li>{ _t("Update any local room aliases to point to the new room") }</li>
+                    <li>{ _t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room") }</li>
+                    <li>{ _t("Put a link back to the old room at the start of the new room so people can see old messages") }</li>
                 </ol>
-                {buttons}
+                { buttons }
             </BaseDialog>
         );
     }
diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.js b/src/components/views/dialogs/RoomUpgradeWarningDialog.js
index c73edcd871..29adb261be 100644
--- a/src/components/views/dialogs/RoomUpgradeWarningDialog.js
+++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.js
@@ -84,16 +84,16 @@ export default class RoomUpgradeWarningDialog extends React.Component {
 
         let bugReports = (
             <p>
-                {_t(
+                { _t(
                     "This usually only affects how the room is processed on the server. If you're " +
                     "having problems with your %(brand)s, please report a bug.", { brand },
-                )}
+                ) }
             </p>
         );
         if (SdkConfig.get().bug_report_endpoint_url) {
             bugReports = (
                 <p>
-                    {_t(
+                    { _t(
                         "This usually only affects how the room is processed on the server. If you're " +
                         "having problems with your %(brand)s, please <a>report a bug</a>.",
                         {
@@ -101,10 +101,10 @@ export default class RoomUpgradeWarningDialog extends React.Component {
                         },
                         {
                             "a": (sub) => {
-                                return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
+                                return <a href='#' onClick={this._openBugReportDialog}>{ sub }</a>;
                             },
                         },
-                    )}
+                    ) }
                 </p>
             );
         }
@@ -119,23 +119,23 @@ export default class RoomUpgradeWarningDialog extends React.Component {
             >
                 <div>
                     <p>
-                        {_t(
+                        { _t(
                             "Upgrading a room is an advanced action and is usually recommended when a room " +
                             "is unstable due to bugs, missing features or security vulnerabilities.",
-                        )}
+                        ) }
                     </p>
-                    {bugReports}
+                    { bugReports }
                     <p>
-                        {_t(
+                        { _t(
                             "You'll upgrade this room from <oldVersion /> to <newVersion />.",
                             {},
                             {
-                                oldVersion: () => <code>{this.state.currentVersion}</code>,
-                                newVersion: () => <code>{this.props.targetVersion}</code>,
+                                oldVersion: () => <code>{ this.state.currentVersion }</code>,
+                                newVersion: () => <code>{ this.props.targetVersion }</code>,
                             },
-                        )}
+                        ) }
                     </p>
-                    {inviteToggle}
+                    { inviteToggle }
                 </div>
                 <DialogButtons
                     primaryButton={_t("Upgrade")}
diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx
index ebf32e9131..ff24a1bad8 100644
--- a/src/components/views/dialogs/ServerOfflineDialog.tsx
+++ b/src/components/views/dialogs/ServerOfflineDialog.tsx
@@ -54,7 +54,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
             const header = (
                 <div className="mx_ServerOfflineDialog_content_context_timeline_header">
                     <RoomAvatar width={24} height={24} room={c.room} />
-                    <span>{c.room.name}</span>
+                    <span>{ c.room.name }</span>
                 </div>
             );
             const entries = c.transactions
@@ -63,26 +63,26 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
                     let button = <Spinner w={19} h={19} />;
                     if (t.status === TransactionStatus.Error) {
                         button = (
-                            <AccessibleButton kind="link" onClick={() => t.run()}>{_t("Resend")}</AccessibleButton>
+                            <AccessibleButton kind="link" onClick={() => t.run()}>{ _t("Resend") }</AccessibleButton>
                         );
                     }
                     return (
                         <div className="mx_ServerOfflineDialog_content_context_txn" key={`txn-${j}`}>
                             <span className="mx_ServerOfflineDialog_content_context_txn_desc">
-                                {t.auditName}
+                                { t.auditName }
                             </span>
-                            {button}
+                            { button }
                         </div>
                     );
                 });
             return (
                 <div className="mx_ServerOfflineDialog_content_context" key={`context-${i}`}>
                     <div className="mx_ServerOfflineDialog_content_context_timestamp">
-                        {formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps"))}
+                        { formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps")) }
                     </div>
                     <div className="mx_ServerOfflineDialog_content_context_timeline">
-                        {header}
-                        {entries}
+                        { header }
+                        { entries }
                     </div>
                 </div>
             );
@@ -92,7 +92,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
     public render() {
         let timeline = this.renderTimeline().filter(c => !!c); // remove nulls for next check
         if (timeline.length === 0) {
-            timeline = [<div key={1}>{_t("You're all caught up.")}</div>];
+            timeline = [<div key={1}>{ _t("You're all caught up.") }</div>];
         }
 
         const serverName = MatrixClientPeg.getHomeserverName();
@@ -103,23 +103,23 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
             hasCancel={true}
         >
             <div className="mx_ServerOfflineDialog_content">
-                <p>{_t(
+                <p>{ _t(
                     "Your server isn't responding to some of your requests. " +
                     "Below are some of the most likely reasons.",
-                )}</p>
+                ) }</p>
                 <ul>
-                    <li>{_t("The server (%(serverName)s) took too long to respond.", { serverName })}</li>
-                    <li>{_t("Your firewall or anti-virus is blocking the request.")}</li>
-                    <li>{_t("A browser extension is preventing the request.")}</li>
-                    <li>{_t("The server is offline.")}</li>
-                    <li>{_t("The server has denied your request.")}</li>
-                    <li>{_t("Your area is experiencing difficulties connecting to the internet.")}</li>
-                    <li>{_t("A connection error occurred while trying to contact the server.")}</li>
-                    <li>{_t("The server is not configured to indicate what the problem is (CORS).")}</li>
+                    <li>{ _t("The server (%(serverName)s) took too long to respond.", { serverName }) }</li>
+                    <li>{ _t("Your firewall or anti-virus is blocking the request.") }</li>
+                    <li>{ _t("A browser extension is preventing the request.") }</li>
+                    <li>{ _t("The server is offline.") }</li>
+                    <li>{ _t("The server has denied your request.") }</li>
+                    <li>{ _t("Your area is experiencing difficulties connecting to the internet.") }</li>
+                    <li>{ _t("A connection error occurred while trying to contact the server.") }</li>
+                    <li>{ _t("The server is not configured to indicate what the problem is (CORS).") }</li>
                 </ul>
                 <hr />
-                <h2>{_t("Recent changes that have not yet been received")}</h2>
-                {timeline}
+                <h2>{ _t("Recent changes that have not yet been received") }</h2>
+                { timeline }
             </div>
         </BaseDialog>;
     }
diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx
index d480ca2043..7a79791b3c 100644
--- a/src/components/views/dialogs/ServerPickerDialog.tsx
+++ b/src/components/views/dialogs/ServerPickerDialog.tsx
@@ -172,7 +172,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
         if (this.defaultServer.hsNameIsDifferent) {
             defaultServerName = (
                 <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}>
-                    {this.defaultServer.hsName}
+                    { this.defaultServer.hsName }
                 </TextWithTooltip>
             );
         }
@@ -187,7 +187,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
         >
             <form className="mx_Dialog_content" id="mx_ServerPickerDialog" onSubmit={this.onSubmit}>
                 <p>
-                    {_t("We call the places where you can host your account ‘homeservers’.")} {text}
+                    { _t("We call the places where you can host your account ‘homeservers’.") } { text }
                 </p>
 
                 <StyledRadioButton
@@ -196,7 +196,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
                     checked={this.state.defaultChosen}
                     onChange={this.onDefaultChosen}
                 >
-                    {defaultServerName}
+                    { defaultServerName }
                 </StyledRadioButton>
 
                 <StyledRadioButton
@@ -222,16 +222,16 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
                     />
                 </StyledRadioButton>
                 <p>
-                    {_t("Use your preferred Matrix homeserver if you have one, or host your own.")}
+                    { _t("Use your preferred Matrix homeserver if you have one, or host your own.") }
                 </p>
 
                 <AccessibleButton className="mx_ServerPickerDialog_continue" kind="primary" onClick={this.onSubmit}>
-                    {_t("Continue")}
+                    { _t("Continue") }
                 </AccessibleButton>
 
-                <h4>{_t("Learn more")}</h4>
+                <h4>{ _t("Learn more") }</h4>
                 <a href="https://matrix.org/faq/#what-is-a-homeserver%3F" target="_blank" rel="noreferrer noopener">
-                    {_t("About homeservers")}
+                    { _t("About homeservers") }
                 </a>
             </form>
         </BaseDialog>;
diff --git a/src/components/views/dialogs/SeshatResetDialog.tsx b/src/components/views/dialogs/SeshatResetDialog.tsx
index 863157ec08..b89002c30f 100644
--- a/src/components/views/dialogs/SeshatResetDialog.tsx
+++ b/src/components/views/dialogs/SeshatResetDialog.tsx
@@ -33,12 +33,12 @@ export default class SeshatResetDialog extends React.PureComponent<IDialogProps>
                 title={_t("Reset event store?")}>
                 <div>
                     <p>
-                        {_t("You most likely do not want to reset your event index store")}
+                        { _t("You most likely do not want to reset your event index store") }
                         <br />
-                        {_t("If you do, please note that none of your messages will be deleted, " +
+                        { _t("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",
-                        )}
+                        ) }
                     </p>
                 </div>
                 <DialogButtons
diff --git a/src/components/views/dialogs/SlashCommandHelpDialog.js b/src/components/views/dialogs/SlashCommandHelpDialog.js
index 608f81a612..d21ccbe47f 100644
--- a/src/components/views/dialogs/SlashCommandHelpDialog.js
+++ b/src/components/views/dialogs/SlashCommandHelpDialog.js
@@ -35,16 +35,16 @@ export default ({ onFinished }) => {
         const rows = [
             <tr key={"_category_" + category} className="mx_SlashCommandHelpDialog_headerRow">
                 <td colSpan={3}>
-                    <h2>{_t(category)}</h2>
+                    <h2>{ _t(category) }</h2>
                 </td>
             </tr>,
         ];
 
         categories[category].forEach(cmd => {
             rows.push(<tr key={cmd.command}>
-                <td><strong>{cmd.getCommand()}</strong></td>
-                <td>{cmd.args}</td>
-                <td>{cmd.description}</td>
+                <td><strong>{ cmd.getCommand() }</strong></td>
+                <td>{ cmd.args }</td>
+                <td>{ cmd.description }</td>
             </tr>);
         });
 
@@ -56,7 +56,7 @@ export default ({ onFinished }) => {
         title={_t("Command Help")}
         description={<table>
             <tbody>
-                {body}
+                { body }
             </tbody>
         </table>}
         hasCloseButton={true}
diff --git a/src/components/views/dialogs/StorageEvictedDialog.js b/src/components/views/dialogs/StorageEvictedDialog.js
index c25866b64d..124e1763c9 100644
--- a/src/components/views/dialogs/StorageEvictedDialog.js
+++ b/src/components/views/dialogs/StorageEvictedDialog.js
@@ -48,7 +48,7 @@ export default class StorageEvictedDialog extends React.Component {
                 "To help us prevent this in future, please <a>send us logs</a>.",
                 {},
                 {
-                    a: text => <a href="#" onClick={this._sendBugReport}>{text}</a>,
+                    a: text => <a href="#" onClick={this._sendBugReport}>{ text }</a>,
                 },
             );
         }
@@ -60,15 +60,15 @@ export default class StorageEvictedDialog extends React.Component {
                 hasCancel={false}
             >
                 <div className="mx_Dialog_content" id='mx_Dialog_content'>
-                    <p>{_t(
+                    <p>{ _t(
                         "Some session data, including encrypted message keys, is " +
                         "missing. Sign out and sign in to fix this, restoring keys " +
                         "from backup.",
-                    )}</p>
-                    <p>{_t(
+                    ) }</p>
+                    <p>{ _t(
                         "Your browser likely removed this data when running low on " +
                         "disk space.",
-                    )} {logRequest}</p>
+                    ) } { logRequest }</p>
                 </div>
                 <DialogButtons primaryButton={_t("Sign out")}
                     onPrimaryButtonClick={this._onSignOutClick}
diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
index 3a3cc31cf8..8723d4a453 100644
--- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
+++ b/src/components/views/dialogs/TabbedIntegrationManagerDialog.js
@@ -134,7 +134,7 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
                     key={`tab_${i}`}
                     disabled={this.state.busy}
                 >
-                    {m.name}
+                    { m.name }
                 </AccessibleButton>
             );
         });
@@ -163,10 +163,10 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
         return (
             <div className='mx_TabbedIntegrationManagerDialog_container'>
                 <div className='mx_TabbedIntegrationManagerDialog_tabs'>
-                    {this._renderTabs()}
+                    { this._renderTabs() }
                 </div>
                 <div className='mx_TabbedIntegrationManagerDialog_currentManager'>
-                    {this._renderTab()}
+                    { this._renderTab() }
                 </div>
             </div>
         );
diff --git a/src/components/views/dialogs/TermsDialog.tsx b/src/components/views/dialogs/TermsDialog.tsx
index 58126f77c3..6aba597aad 100644
--- a/src/components/views/dialogs/TermsDialog.tsx
+++ b/src/components/views/dialogs/TermsDialog.tsx
@@ -90,9 +90,9 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
     private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
         switch (serviceType) {
             case SERVICE_TYPES.IS:
-                return <div>{_t("Identity server")}<br />({host})</div>;
+                return <div>{ _t("Identity server") }<br />({ host })</div>;
             case SERVICE_TYPES.IM:
-                return <div>{_t("Integration manager")}<br />({host})</div>;
+                return <div>{ _t("Integration manager") }<br />({ host })</div>;
         }
     }
 
@@ -100,13 +100,13 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
         switch (serviceType) {
             case SERVICE_TYPES.IS:
                 return <div>
-                    {_t("Find others by phone or email")}
+                    { _t("Find others by phone or email") }
                     <br />
-                    {_t("Be found by phone or email")}
+                    { _t("Be found by phone or email") }
                 </div>;
             case SERVICE_TYPES.IM:
                 return <div>
-                    {_t("Use bots, bridges, widgets and sticker packs")}
+                    { _t("Use bots, bridges, widgets and sticker packs") }
                 </div>;
         }
     }
@@ -136,10 +136,10 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
                 }
 
                 rows.push(<tr key={termDoc[termsLang].url}>
-                    <td className="mx_TermsDialog_service">{serviceName}</td>
-                    <td className="mx_TermsDialog_summary">{summary}</td>
+                    <td className="mx_TermsDialog_service">{ serviceName }</td>
+                    <td className="mx_TermsDialog_summary">{ summary }</td>
                     <td>
-                        {termDoc[termsLang].name}
+                        { termDoc[termsLang].name }
                         <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
                             <span className="mx_TermsDialog_link" />
                         </a>
@@ -186,16 +186,16 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
                 hasCancel={false}
             >
                 <div id='mx_Dialog_content'>
-                    <p>{_t("To continue you need to accept the terms of this service.")}</p>
+                    <p>{ _t("To continue you need to accept the terms of this service.") }</p>
 
                     <table className="mx_TermsDialog_termsTable"><tbody>
                         <tr className="mx_TermsDialog_termsTableHeader">
-                            <th>{_t("Service")}</th>
-                            <th>{_t("Summary")}</th>
-                            <th>{_t("Document")}</th>
-                            <th>{_t("Accept")}</th>
+                            <th>{ _t("Service") }</th>
+                            <th>{ _t("Summary") }</th>
+                            <th>{ _t("Document") }</th>
+                            <th>{ _t("Accept") }</th>
                         </tr>
-                        {rows}
+                        { rows }
                     </tbody></table>
                 </div>
 
diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx
index b89293b386..8389757347 100644
--- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx
+++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx
@@ -48,13 +48,13 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
         className="mx_UntrustedDeviceDialog"
         title={<>
             <E2EIcon status="warning" size={24} hideTooltip={true} />
-            { _t("Not Trusted")}
+            { _t("Not Trusted") }
         </>}
     >
         <div className="mx_Dialog_content" id='mx_Dialog_content'>
-            <p>{newSessionText}</p>
-            <p>{device.getDisplayName()} ({device.deviceId})</p>
-            <p>{askToVerifyText}</p>
+            <p>{ newSessionText }</p>
+            <p>{ device.getDisplayName() } ({ device.deviceId })</p>
+            <p>{ askToVerifyText }</p>
         </div>
         <div className='mx_Dialog_buttons'>
             <AccessibleButton element="button" kind="secondary" onClick={() => onFinished("legacy")}>
diff --git a/src/components/views/dialogs/UploadConfirmDialog.tsx b/src/components/views/dialogs/UploadConfirmDialog.tsx
index e68067cd2b..508bb95e43 100644
--- a/src/components/views/dialogs/UploadConfirmDialog.tsx
+++ b/src/components/views/dialogs/UploadConfirmDialog.tsx
@@ -86,7 +86,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
             preview = <div className="mx_UploadConfirmDialog_previewOuter">
                 <div className="mx_UploadConfirmDialog_previewInner">
                     <div><img className="mx_UploadConfirmDialog_imagePreview" src={this.objectUrl} /></div>
-                    <div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
+                    <div>{ this.props.file.name } ({ filesize(this.props.file.size) })</div>
                 </div>
             </div>;
         } else {
@@ -95,7 +95,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
                     <img className="mx_UploadConfirmDialog_fileIcon"
                         src={require("../../../../res/img/feather-customised/files.svg")}
                     />
-                    {this.props.file.name} ({filesize(this.props.file.size)})
+                    { this.props.file.name } ({ filesize(this.props.file.size) })
                 </div>
             </div>;
         }
@@ -103,7 +103,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
         let uploadAllButton;
         if (this.props.currentIndex + 1 < this.props.totalFiles) {
             uploadAllButton = <button onClick={this.onUploadAllClick}>
-                {_t("Upload all")}
+                { _t("Upload all") }
             </button>;
         }
 
@@ -115,7 +115,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
                 contentId='mx_Dialog_content'
             >
                 <div id='mx_Dialog_content'>
-                    {preview}
+                    { preview }
                 </div>
 
                 <DialogButtons primaryButton={_t('Upload')}
@@ -123,7 +123,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
                     onPrimaryButtonClick={this.onUploadClick}
                     focus={true}
                 >
-                    {uploadAllButton}
+                    { uploadAllButton }
                 </DialogButtons>
             </BaseDialog>
         );
diff --git a/src/components/views/dialogs/UploadFailureDialog.js b/src/components/views/dialogs/UploadFailureDialog.js
index d26b83d0d6..224098f935 100644
--- a/src/components/views/dialogs/UploadFailureDialog.js
+++ b/src/components/views/dialogs/UploadFailureDialog.js
@@ -60,7 +60,7 @@ export default class UploadFailureDialog extends React.Component {
                     limit: filesize(this.props.contentMessages.getUploadLimit()),
                     sizeOfThisFile: filesize(this.props.badFiles[0].size),
                 }, {
-                    b: sub => <b>{sub}</b>,
+                    b: sub => <b>{ sub }</b>,
                 },
             );
             buttons = <DialogButtons primaryButton={_t('OK')}
@@ -75,7 +75,7 @@ export default class UploadFailureDialog extends React.Component {
                 {
                     limit: filesize(this.props.contentMessages.getUploadLimit()),
                 }, {
-                    b: sub => <b>{sub}</b>,
+                    b: sub => <b>{ sub }</b>,
                 },
             );
             buttons = <DialogButtons primaryButton={_t('OK')}
@@ -90,7 +90,7 @@ export default class UploadFailureDialog extends React.Component {
                 {
                     limit: filesize(this.props.contentMessages.getUploadLimit()),
                 }, {
-                    b: sub => <b>{sub}</b>,
+                    b: sub => <b>{ sub }</b>,
                 },
             );
             const howManyOthers = this.props.totalFiles - this.props.badFiles.length;
@@ -111,11 +111,11 @@ export default class UploadFailureDialog extends React.Component {
                 contentId='mx_Dialog_content'
             >
                 <div id='mx_Dialog_content'>
-                    {message}
-                    {preview}
+                    { message }
+                    { preview }
                 </div>
 
-                {buttons}
+                { buttons }
             </BaseDialog>
         );
     }
diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx
index e85938afe0..7608d7cb55 100644
--- a/src/components/views/dialogs/UserSettingsDialog.tsx
+++ b/src/components/views/dialogs/UserSettingsDialog.tsx
@@ -81,7 +81,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
         this.setState({ mjolnirEnabled: newValue });
     };
 
-    _getTabs() {
+    private getTabs() {
         const tabs = [];
 
         tabs.push(new Tab(
@@ -170,7 +170,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
                 title={_t("Settings")}
             >
                 <div className='mx_SettingsDialog_content'>
-                    <TabbedView tabs={this._getTabs()} initialTabId={this.props.initialTabId} />
+                    <TabbedView tabs={this.getTabs()} initialTabId={this.props.initialTabId} />
                 </div>
             </BaseDialog>
         );
diff --git a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
index 638d5cde93..ebeab191b1 100644
--- a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
+++ b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
@@ -105,7 +105,7 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
         const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => {
             const text = CapabilityText.for(cap, this.props.widgetKind);
             const byline = text.byline
-                ? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{text.byline}</span>
+                ? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{ text.byline }</span>
                 : null;
 
             return (
@@ -113,8 +113,8 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
                     <StyledCheckbox
                         checked={isChecked}
                         onChange={() => this.onToggle(cap)}
-                    >{text.primary}</StyledCheckbox>
-                    {byline}
+                    >{ text.primary }</StyledCheckbox>
+                    { byline }
                 </div>
             );
         });
@@ -127,8 +127,8 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
             >
                 <form onSubmit={this.onSubmit}>
                     <div className="mx_Dialog_content">
-                        <div className="text-muted">{_t("This widget would like to:")}</div>
-                        {checkboxRows}
+                        <div className="text-muted">{ _t("This widget would like to:") }</div>
+                        { checkboxRows }
                         <DialogButtons
                             primaryButton={_t("Approve")}
                             cancelButton={_t("Decline All")}
diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
index 2130d0e4ef..1bc6444ac1 100644
--- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
+++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
@@ -78,11 +78,11 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component {
             >
                 <div className='mx_WidgetOpenIDPermissionsDialog_content'>
                     <p>
-                        {_t("The widget will verify your user ID, but won't be able to perform actions for you:")}
+                        { _t("The widget will verify your user ID, but won't be able to perform actions for you:") }
                     </p>
                     <p className="text-muted">
-                        {/* cheap trim to just get the path */}
-                        {this.props.widget.templateUrl.split("?")[0].split("#")[0]}
+                        { /* cheap trim to just get the path */ }
+                        { this.props.widget.templateUrl.split("?")[0].split("#")[0] }
                     </p>
                 </div>
                 <DialogButtons
diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx
index 90c640977c..b425f37dce 100644
--- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx
+++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx
@@ -285,11 +285,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
 
         const resetButton = (
             <div className="mx_AccessSecretStorageDialog_reset">
-                {_t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
+                { _t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, {
                     a: (sub) => <a
                         href="" onClick={this.onResetAllClick}
-                        className="mx_AccessSecretStorageDialog_reset_link">{sub}</a>,
-                })}
+                        className="mx_AccessSecretStorageDialog_reset_link">{ sub }</a>,
+                }) }
             </div>
         );
 
@@ -300,9 +300,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
             title = _t("Reset everything");
             titleClass = ['mx_AccessSecretStorageDialog_titleWithIcon mx_AccessSecretStorageDialog_resetBadge'];
             content = <div>
-                <p>{_t("Only do this if you have no other device to complete verification with.")}</p>
-                <p>{_t("If you reset everything, you will restart with no trusted sessions, no trusted users, and "
-                    + "might not be able to see past messages.")}</p>
+                <p>{ _t("Only do this if you have no other device to complete verification with.") }</p>
+                <p>{ _t("If you reset everything, you will restart with no trusted sessions, no trusted users, and "
+                    + "might not be able to see past messages.") }</p>
                 <DialogButtons
                     primaryButton={_t('Reset')}
                     onPrimaryButtonClick={this.onConfirmResetAllClick}
@@ -320,27 +320,27 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
             let keyStatus;
             if (this.state.keyMatches === false) {
                 keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
-                    {"\uD83D\uDC4E "}{_t(
+                    { "\uD83D\uDC4E " }{ _t(
                         "Unable to access secret storage. " +
                         "Please verify that you entered the correct Security Phrase.",
-                    )}
+                    ) }
                 </div>;
             } else {
                 keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
             }
 
             content = <div>
-                <p>{_t(
+                <p>{ _t(
                     "Enter your Security Phrase or <button>Use your Security Key</button> to continue.", {},
                     {
                         button: s => <AccessibleButton className="mx_linkButton"
                             element="span"
                             onClick={this.onUseRecoveryKeyClick}
                         >
-                            {s}
+                            { s }
                         </AccessibleButton>,
                     },
-                )}</p>
+                ) }</p>
 
                 <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
                     <input
@@ -353,7 +353,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
                         autoComplete="new-password"
                         placeholder={_t("Security Phrase")}
                     />
-                    {keyStatus}
+                    { keyStatus }
                     <DialogButtons
                         primaryButton={_t('Continue')}
                         onPrimaryButtonClick={this.onPassPhraseNext}
@@ -375,11 +375,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
                 'mx_AccessSecretStorageDialog_recoveryKeyFeedback_invalid': this.state.recoveryKeyCorrect === false,
             });
             const recoveryKeyFeedback = <div className={feedbackClasses}>
-                {this.getKeyValidationText()}
+                { this.getKeyValidationText() }
             </div>;
 
             content = <div>
-                <p>{_t("Use your Security Key to continue.")}</p>
+                <p>{ _t("Use your Security Key to continue.") }</p>
 
                 <form
                     className="mx_AccessSecretStorageDialog_primaryContainer"
@@ -399,7 +399,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
                             />
                         </div>
                         <span className="mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText">
-                            {_t("or")}
+                            { _t("or") }
                         </span>
                         <div>
                             <input type="file"
@@ -408,11 +408,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
                                 onChange={this.onRecoveryKeyFileChange}
                             />
                             <AccessibleButton kind="primary" onClick={this.onRecoveryKeyFileUploadClick}>
-                                {_t("Upload")}
+                                { _t("Upload") }
                             </AccessibleButton>
                         </div>
                     </div>
-                    {recoveryKeyFeedback}
+                    { recoveryKeyFeedback }
                     <DialogButtons
                         primaryButton={_t('Continue')}
                         onPrimaryButtonClick={this.onRecoveryKeyNext}
@@ -435,7 +435,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
                 titleClass={titleClass}
             >
                 <div>
-                    {content}
+                    { content }
                 </div>
             </BaseDialog>
         );
diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx
index c0530a35ea..392598ca36 100644
--- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx
+++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx
@@ -44,12 +44,12 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component<IP
             >
                 <div className='mx_ConfirmDestroyCrossSigningDialog_content'>
                     <p>
-                        {_t(
+                        { _t(
                             "Deleting cross-signing keys is permanent. " +
                             "Anyone you have verified with will see security alerts. " +
                             "You almost certainly don't want to do this, unless " +
                             "you've lost every device you can cross-sign from.",
-                        )}
+                        ) }
                     </p>
                 </div>
                 <DialogButtons
diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
index 84dcfede4a..c447bfa859 100644
--- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
+++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
@@ -175,7 +175,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent<IProps
         let content;
         if (this.state.error) {
             content = <div>
-                <p>{_t("Unable to set up keys")}</p>
+                <p>{ _t("Unable to set up keys") }</p>
                 <div className="mx_Dialog_buttons">
                     <DialogButtons primaryButton={_t('Retry')}
                         onPrimaryButtonClick={this.bootstrapCrossSigning}
@@ -197,7 +197,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent<IProps
                 fixedWidth={false}
             >
                 <div>
-                    {content}
+                    { content }
                 </div>
             </BaseDialog>
         );
diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js
index 5f21033d29..e8bd8af01c 100644
--- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js
+++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js
@@ -288,7 +288,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
                 details = _t("Fetching keys from server...");
             }
             content = <div>
-                <div>{details}</div>
+                <div>{ details }</div>
                 <Spinner />
             </div>;
         } else if (this.state.loadError) {
@@ -299,18 +299,18 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
                 if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
                     title = _t("Security Key mismatch");
                     content = <div>
-                        <p>{_t(
+                        <p>{ _t(
                             "Backup could not be decrypted with this Security Key: " +
                             "please verify that you entered the correct Security Key.",
-                        )}</p>
+                        ) }</p>
                     </div>;
                 } else {
                     title = _t("Incorrect Security Phrase");
                     content = <div>
-                        <p>{_t(
+                        <p>{ _t(
                             "Backup could not be decrypted with this Security Phrase: " +
                             "please verify that you entered the correct Security Phrase.",
-                        )}</p>
+                        ) }</p>
                     </div>;
                 }
             } else {
@@ -325,14 +325,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
             title = _t("Keys restored");
             let failedToDecrypt;
             if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
-                failedToDecrypt = <p>{_t(
+                failedToDecrypt = <p>{ _t(
                     "Failed to decrypt %(failedCount)s sessions!",
                     { failedCount: this.state.recoverInfo.total - this.state.recoverInfo.imported },
-                )}</p>;
+                ) }</p>;
             }
             content = <div>
-                <p>{_t("Successfully restored %(sessionCount)s keys", { sessionCount: this.state.recoverInfo.imported })}</p>
-                {failedToDecrypt}
+                <p>{ _t("Successfully restored %(sessionCount)s keys", { sessionCount: this.state.recoverInfo.imported }) }</p>
+                { failedToDecrypt }
                 <DialogButtons primaryButton={_t('OK')}
                     onPrimaryButtonClick={this._onDone}
                     hasCancel={false}
@@ -344,15 +344,15 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
             const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
             title = _t("Enter Security Phrase");
             content = <div>
-                <p>{_t(
+                <p>{ _t(
                     "<b>Warning</b>: you should only set up key backup " +
                     "from a trusted computer.", {},
-                    { b: sub => <b>{sub}</b> },
-                )}</p>
-                <p>{_t(
+                    { b: sub => <b>{ sub }</b> },
+                ) }</p>
+                <p>{ _t(
                     "Access your secure message history and set up secure " +
                     "messaging by entering your Security Phrase.",
-                )}</p>
+                ) }</p>
 
                 <form className="mx_RestoreKeyBackupDialog_primaryContainer">
                     <input type="password"
@@ -370,7 +370,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
                         focus={false}
                     />
                 </form>
-                {_t(
+                { _t(
                     "If you've forgotten your Security Phrase you can "+
                     "<button1>use your Security Key</button1> or " +
                     "<button2>set up new recovery options</button2>",
@@ -381,16 +381,16 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
                             element="span"
                             onClick={this._onUseRecoveryKeyClick}
                         >
-                            {s}
+                            { s }
                         </AccessibleButton>,
                         button2: s => <AccessibleButton
                             className="mx_linkButton"
                             element="span"
                             onClick={this._onResetRecoveryClick}
                         >
-                            {s}
+                            { s }
                         </AccessibleButton>,
-                    })}
+                    }) }
             </div>;
         } else {
             title = _t("Enter Security Key");
@@ -402,24 +402,24 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
                 keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus"></div>;
             } else if (this.state.recoveryKeyValid) {
                 keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
-                    {"\uD83D\uDC4D "}{_t("This looks like a valid Security Key!")}
+                    { "\uD83D\uDC4D " }{ _t("This looks like a valid Security Key!") }
                 </div>;
             } else {
                 keyStatus = <div className="mx_RestoreKeyBackupDialog_keyStatus">
-                    {"\uD83D\uDC4E "}{_t("Not a valid Security Key")}
+                    { "\uD83D\uDC4E " }{ _t("Not a valid Security Key") }
                 </div>;
             }
 
             content = <div>
-                <p>{_t(
+                <p>{ _t(
                     "<b>Warning</b>: You should only set up key backup " +
                     "from a trusted computer.", {},
-                    { b: sub => <b>{sub}</b> },
-                )}</p>
-                <p>{_t(
+                    { b: sub => <b>{ sub }</b> },
+                ) }</p>
+                <p>{ _t(
                     "Access your secure message history and set up secure " +
                     "messaging by entering your Security Key.",
-                )}</p>
+                ) }</p>
 
                 <div className="mx_RestoreKeyBackupDialog_primaryContainer">
                     <input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
@@ -427,7 +427,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
                         value={this.state.recoveryKey}
                         autoFocus={true}
                     />
-                    {keyStatus}
+                    { keyStatus }
                     <DialogButtons primaryButton={_t('Next')}
                         onPrimaryButtonClick={this._onRecoveryKeyNext}
                         hasCancel={true}
@@ -436,7 +436,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
                         primaryDisabled={!this.state.recoveryKeyValid}
                     />
                 </div>
-                {_t(
+                { _t(
                     "If you've forgotten your Security Key you can "+
                     "<button>set up new recovery options</button>",
                     {},
@@ -445,10 +445,10 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
                             element="span"
                             onClick={this._onResetRecoveryClick}
                         >
-                            {s}
+                            { s }
                         </AccessibleButton>,
                     },
-                )}
+                ) }
             </div>;
         }
 
@@ -458,7 +458,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
                 title={title}
             >
                 <div className='mx_RestoreKeyBackupDialog_content'>
-                    {content}
+                    { content }
                 </div>
             </BaseDialog>
         );
diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx
index e4a967fbdc..dbad2ca024 100644
--- a/src/components/views/directory/NetworkDropdown.tsx
+++ b/src/components/views/directory/NetworkDropdown.tsx
@@ -184,7 +184,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
             if (server === hsName) {
                 subtitle = (
                     <div className="mx_NetworkDropdown_server_subtitle">
-                        {_t("Your server")}
+                        { _t("Your server") }
                     </div>
                 );
             }
@@ -238,7 +238,7 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
                         label={_t("Matrix")}
                         className="mx_NetworkDropdown_server_network"
                     >
-                        {_t("Matrix")}
+                        { _t("Matrix") }
                     </MenuItemRadio>
                     { entries }
                 </MenuGroup>
@@ -270,9 +270,9 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
         const buttonRect = handle.current.getBoundingClientRect();
         content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu}>
             <div className="mx_NetworkDropdown_menu">
-                {options}
+                { options }
                 <MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
-                    {_t("Add a new server...")}
+                    { _t("Add a new server...") }
                 </MenuItem>
             </div>
         </ContextMenu>;
@@ -295,15 +295,15 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
             isExpanded={menuDisplayed}
         >
             <span>
-                {currentValue}
+                { currentValue }
             </span> <span className="mx_NetworkDropdown_handle_server">
-                ({selectedServerName})
+                ({ selectedServerName })
             </span>
         </ContextMenuButton>;
     }
 
     return <div className="mx_NetworkDropdown" ref={handle}>
-        {content}
+        { content }
     </div>;
 };
 
diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.js
index c1f370b626..a7d249164b 100644
--- a/src/components/views/elements/AppPermission.js
+++ b/src/components/views/elements/AppPermission.js
@@ -94,15 +94,15 @@ export default class AppPermission extends React.Component {
 
         const warningTooltipText = (
             <div>
-                {_t("Any of the following data may be shared:")}
+                { _t("Any of the following data may be shared:") }
                 <ul>
-                    <li>{_t("Your display name")}</li>
-                    <li>{_t("Your avatar URL")}</li>
-                    <li>{_t("Your user ID")}</li>
-                    <li>{_t("Your theme")}</li>
-                    <li>{_t("%(brand)s URL", { brand })}</li>
-                    <li>{_t("Room ID")}</li>
-                    <li>{_t("Widget ID")}</li>
+                    <li>{ _t("Your display name") }</li>
+                    <li>{ _t("Your avatar URL") }</li>
+                    <li>{ _t("Your user ID") }</li>
+                    <li>{ _t("Your theme") }</li>
+                    <li>{ _t("%(brand)s URL", { brand }) }</li>
+                    <li>{ _t("Room ID") }</li>
+                    <li>{ _t("Widget ID") }</li>
                 </ul>
             </div>
         );
@@ -124,22 +124,22 @@ export default class AppPermission extends React.Component {
         return (
             <div className='mx_AppPermissionWarning'>
                 <div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_bolder mx_AppPermissionWarning_smallText'>
-                    {_t("Widget added by")}
+                    { _t("Widget added by") }
                 </div>
                 <div className='mx_AppPermissionWarning_row'>
-                    {avatar}
-                    <h4 className='mx_AppPermissionWarning_bolder'>{displayName}</h4>
-                    <div className='mx_AppPermissionWarning_smallText'>{userId}</div>
+                    { avatar }
+                    <h4 className='mx_AppPermissionWarning_bolder'>{ displayName }</h4>
+                    <div className='mx_AppPermissionWarning_smallText'>{ userId }</div>
                 </div>
                 <div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
-                    {warning}
+                    { warning }
                 </div>
                 <div className='mx_AppPermissionWarning_row mx_AppPermissionWarning_smallText'>
-                    {_t("This widget may use cookies.")}&nbsp;{encryptionWarning}
+                    { _t("This widget may use cookies.") }&nbsp;{ encryptionWarning }
                 </div>
                 <div className='mx_AppPermissionWarning_row'>
                     <AccessibleButton kind='primary_sm' onClick={this.props.onPermissionGranted}>
-                        {_t("Continue")}
+                        { _t("Continue") }
                     </AccessibleButton>
                 </div>
             </div>
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 7e98537180..74ef178066 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -408,7 +408,7 @@ export default class AppTile extends React.Component {
                     // AppTile's border is in the wrong place
                     appTileBody = <div className="mx_AppTile_persistedWrapper">
                         <PersistedElement persistKey={this._persistKey}>
-                            {appTileBody}
+                            { appTileBody }
                         </PersistedElement>
                     </div>;
                 }
@@ -453,13 +453,13 @@ export default class AppTile extends React.Component {
                             title={_t('Popout widget')}
                             onClick={this._onPopoutWidgetClick}
                         /> }
-                        { <ContextMenuButton
+                        <ContextMenuButton
                             className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
                             label={_t("Options")}
                             isExpanded={this.state.menuDisplayed}
                             inputRef={this._contextMenuButton}
                             onClick={this._onContextMenuClick}
-                        /> }
+                        />
                     </span>
                 </div> }
                 { appTileBody }
diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js
index 2e88d37882..97bae82e61 100644
--- a/src/components/views/elements/DNDTagTile.js
+++ b/src/components/views/elements/DNDTagTile.js
@@ -41,6 +41,6 @@ export default function DNDTagTile(props) {
             menuDisplayed={menuDisplayed}
             openMenu={openMenu}
         />
-        {contextMenu}
+        { contextMenu }
     </>;
 }
diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/DesktopBuildsNotice.tsx
index c97a9b6cef..f2441b83a4 100644
--- a/src/components/views/elements/DesktopBuildsNotice.tsx
+++ b/src/components/views/elements/DesktopBuildsNotice.tsx
@@ -38,7 +38,7 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
 
     if (EventIndexPeg.error) {
         return <>
-            {_t("Message search initialisation failed, check <a>your settings</a> for more information", {}, {
+            { _t("Message search initialisation failed, check <a>your settings</a> for more information", {}, {
                 a: sub => (<a onClick={(evt) => {
                     evt.preventDefault();
                     dis.dispatch({
@@ -46,9 +46,9 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
                         initialTabId: UserTab.Security,
                     });
                 }}>
-                    {sub}
+                    { sub }
                 </a>),
-            })}
+            }) }
         </>;
     }
 
@@ -61,12 +61,12 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
         switch (kind) {
             case WarningKind.Files:
                 text = _t("Use the <a>Desktop app</a> to see all encrypted files", {}, {
-                    a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
+                    a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{ sub }</a>),
                 });
                 break;
             case WarningKind.Search:
                 text = _t("Use the <a>Desktop app</a> to search encrypted messages", {}, {
-                    a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{sub}</a>),
+                    a: sub => (<a href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">{ sub }</a>),
                 });
                 break;
         }
@@ -89,8 +89,8 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
 
     return (
         <div className="mx_DesktopBuildsNotice">
-            {logo}
-            <span>{text}</span>
+            { logo }
+            <span>{ text }</span>
         </div>
     );
 }
diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
index 8f9b847f4f..82d0aa4976 100644
--- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx
+++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
@@ -56,7 +56,7 @@ export class ExistingSource extends React.Component<DesktopCapturerSourceIProps>
                     className="mx_desktopCapturerSourcePicker_stream_thumbnail"
                     src={this.props.source.thumbnailURL}
                 />
-                <span className="mx_desktopCapturerSourcePicker_stream_name">{this.props.source.name}</span>
+                <span className="mx_desktopCapturerSourcePicker_stream_name">{ this.props.source.name }</span>
             </AccessibleButton>
         );
     }
@@ -157,13 +157,13 @@ export default class DesktopCapturerSourcePicker extends React.Component<
                         className={screensButtonStyle}
                         onClick={this.onScreensClick}
                     >
-                        {_t("Screens")}
+                        { _t("Screens") }
                     </AccessibleButton>
                     <AccessibleButton
                         className={windowsButtonStyle}
                         onClick={this.onWindowsClick}
                     >
-                        {_t("Windows")}
+                        { _t("Windows") }
                     </AccessibleButton>
                 </div>
                 <div className="mx_desktopCapturerSourcePicker_panel">
diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js
index af68260563..9dd4a84b9a 100644
--- a/src/components/views/elements/DialogButtons.js
+++ b/src/components/views/elements/DialogButtons.js
@@ -92,7 +92,7 @@ export default class DialogButtons extends React.Component {
 
         let additive = null;
         if (this.props.additive) {
-            additive = <div className="mx_Dialog_buttons_additive">{this.props.additive}</div>;
+            additive = <div className="mx_Dialog_buttons_additive">{ this.props.additive }</div>;
         }
 
         return (
diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.js
index 45270ada64..11b1ed2cd2 100644
--- a/src/components/views/elements/DirectorySearchBox.js
+++ b/src/components/views/elements/DirectorySearchBox.js
@@ -88,7 +88,7 @@ export default class DirectorySearchBox extends React.Component {
         if (this.props.showJoinButton) {
             joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
                 onClick={this._onJoinButtonClick}
-            >{_t("Join")}</AccessibleButton>;
+            >{ _t("Join") }</AccessibleButton>;
         }
 
         return <div className={`mx_DirectorySearchBox ${this.props.className} mx_textinput`}>
diff --git a/src/components/views/elements/EditableItemList.tsx b/src/components/views/elements/EditableItemList.tsx
index 89e2e1b8a0..5d6e24ab27 100644
--- a/src/components/views/elements/EditableItemList.tsx
+++ b/src/components/views/elements/EditableItemList.tsx
@@ -63,21 +63,21 @@ export class EditableItem extends React.Component<IItemProps, IItemState> {
             return (
                 <div className="mx_EditableItem">
                     <span className="mx_EditableItem_promptText">
-                        {_t("Are you sure?")}
+                        { _t("Are you sure?") }
                     </span>
                     <AccessibleButton
                         onClick={this.onActuallyRemove}
                         kind="primary_sm"
                         className="mx_EditableItem_confirmBtn"
                     >
-                        {_t("Yes")}
+                        { _t("Yes") }
                     </AccessibleButton>
                     <AccessibleButton
                         onClick={this.onDontRemove}
                         kind="danger_sm"
                         className="mx_EditableItem_confirmBtn"
                     >
-                        {_t("No")}
+                        { _t("No") }
                     </AccessibleButton>
                 </div>
             );
@@ -86,7 +86,7 @@ export class EditableItem extends React.Component<IItemProps, IItemState> {
         return (
             <div className="mx_EditableItem">
                 <div onClick={this.onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
-                <span className="mx_EditableItem_item">{this.props.value}</span>
+                <span className="mx_EditableItem_item">{ this.props.value }</span>
             </div>
         );
     }
@@ -155,7 +155,7 @@ export default class EditableItemList<P = {}> extends React.PureComponent<IProps
     render() {
         const editableItems = this.props.items.map((item, index) => {
             if (!this.props.canRemove) {
-                return <li key={item}>{item}</li>;
+                return <li key={item}>{ item }</li>;
             }
 
             return <EditableItem
@@ -166,7 +166,7 @@ export default class EditableItemList<P = {}> extends React.PureComponent<IProps
             />;
         });
 
-        const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
+        const editableItemsSection = this.props.canRemove ? editableItems : <ul>{ editableItems }</ul>;
         const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
 
         return (
diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx
index f967b8c594..334e569163 100644
--- a/src/components/views/elements/ErrorBoundary.tsx
+++ b/src/components/views/elements/ErrorBoundary.tsx
@@ -81,33 +81,33 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
             let bugReportSection;
             if (SdkConfig.get().bug_report_endpoint_url) {
                 bugReportSection = <React.Fragment>
-                    <p>{_t(
+                    <p>{ _t(
                         "Please <newIssueLink>create a new issue</newIssueLink> " +
                         "on GitHub so that we can investigate this bug.", {}, {
                             newIssueLink: (sub) => {
                                 return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
                             },
                         },
-                    )}</p>
-                    <p>{_t(
+                    ) }</p>
+                    <p>{ _t(
                         "If you've submitted a bug via GitHub, debug logs can help " +
                         "us track down the problem. Debug logs contain application " +
                         "usage data including your username, the IDs or aliases of " +
                         "the rooms or groups you have visited and the usernames of " +
                         "other users. They do not contain messages.",
-                    )}</p>
+                    ) }</p>
                     <AccessibleButton onClick={this.onBugReport} kind='primary'>
-                        {_t("Submit debug logs")}
+                        { _t("Submit debug logs") }
                     </AccessibleButton>
                 </React.Fragment>;
             }
 
             return <div className="mx_ErrorBoundary">
                 <div className="mx_ErrorBoundary_body">
-                    <h1>{_t("Something went wrong!")}</h1>
+                    <h1>{ _t("Something went wrong!") }</h1>
                     { bugReportSection }
                     <AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
-                        {_t("Clear cache and reload")}
+                        { _t("Clear cache and reload") }
                     </AccessibleButton>
                 </div>
             </div>;
diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx
index 681817ca86..b1cc9c773d 100644
--- a/src/components/views/elements/EventListSummary.tsx
+++ b/src/components/views/elements/EventListSummary.tsx
@@ -63,7 +63,7 @@ const EventListSummary: React.FC<IProps> = ({
     // If we are only given few events then just pass them through
     if (events.length < threshold) {
         return (
-            <li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
+            <li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true}>
                 { children }
             </li>
         );
@@ -92,7 +92,7 @@ const EventListSummary: React.FC<IProps> = ({
     }
 
     return (
-        <li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
+        <li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""}>
             <AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
                 { expanded ? _t('collapse') : _t('expand') }
             </AccessibleButton>
@@ -101,4 +101,8 @@ const EventListSummary: React.FC<IProps> = ({
     );
 };
 
+EventListSummary.defaultProps = {
+    startExpanded: false,
+};
+
 export default EventListSummary;
diff --git a/src/components/views/elements/FacePile.tsx b/src/components/views/elements/FacePile.tsx
index aeca2e844b..0c19a7a63a 100644
--- a/src/components/views/elements/FacePile.tsx
+++ b/src/components/views/elements/FacePile.tsx
@@ -78,7 +78,7 @@ const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, .
         <TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}>
             { members.length > numShown ? <span className="mx_FacePile_face mx_FacePile_more" /> : null }
             { shownMembers.map(m =>
-                <MemberAvatar key={m.userId} member={m} width={28} height={28} className="mx_FacePile_face" /> )}
+                <MemberAvatar key={m.userId} member={m} width={28} height={28} className="mx_FacePile_face" /> ) }
         </TextWithTooltip>
         { onlyKnownUsers && <span className="mx_FacePile_summary">
             { _t("%(count)s people you know have already joined", { count: members.length }) }
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 60f029c32e..5713518eb8 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -240,11 +240,11 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
 
         let prefixContainer = null;
         if (prefixComponent) {
-            prefixContainer = <span className="mx_Field_prefix">{prefixComponent}</span>;
+            prefixContainer = <span className="mx_Field_prefix">{ prefixComponent }</span>;
         }
         let postfixContainer = null;
         if (postfixComponent) {
-            postfixContainer = <span className="mx_Field_postfix">{postfixComponent}</span>;
+            postfixContainer = <span className="mx_Field_postfix">{ postfixComponent }</span>;
         }
 
         const hasValidationFlag = forceValidity !== null && forceValidity !== undefined;
@@ -273,11 +273,11 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
         }
 
         return <div className={fieldClasses}>
-            {prefixContainer}
-            {fieldInput}
-            <label htmlFor={this.id}>{this.props.label}</label>
-            {postfixContainer}
-            {fieldTooltip}
+            { prefixContainer }
+            { fieldInput }
+            <label htmlFor={this.id}>{ this.props.label }</label>
+            { postfixContainer }
+            { fieldTooltip }
         </div>;
     }
 }
diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx
index 84a8733cd0..954c1ab783 100644
--- a/src/components/views/elements/ImageView.tsx
+++ b/src/components/views/elements/ImageView.tsx
@@ -457,7 +457,7 @@ export default class ImageView extends React.Component<IProps, IState> {
                         <AccessibleTooltipButton
                             className="mx_ImageView_button mx_ImageView_button_rotateCCW"
                             title={_t("Rotate Left")}
-                            onClick={ this.onRotateCounterClockwiseClick }>
+                            onClick={this.onRotateCounterClockwiseClick}>
                         </AccessibleTooltipButton>
                         <AccessibleTooltipButton
                             className="mx_ImageView_button mx_ImageView_button_rotateCW"
@@ -467,13 +467,13 @@ export default class ImageView extends React.Component<IProps, IState> {
                         <AccessibleTooltipButton
                             className="mx_ImageView_button mx_ImageView_button_download"
                             title={_t("Download")}
-                            onClick={ this.onDownloadClick }>
+                            onClick={this.onDownloadClick}>
                         </AccessibleTooltipButton>
                         { contextMenuButton }
                         <AccessibleTooltipButton
                             className="mx_ImageView_button mx_ImageView_button_close"
                             title={_t("Close")}
-                            onClick={ this.props.onFinished }>
+                            onClick={this.props.onFinished}>
                         </AccessibleTooltipButton>
                         { this.renderContextMenu() }
                     </div>
diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx
index de82c5daeb..123e118965 100644
--- a/src/components/views/elements/InfoTooltip.tsx
+++ b/src/components/views/elements/InfoTooltip.tsx
@@ -22,9 +22,16 @@ import Tooltip, { Alignment } from './Tooltip';
 import { _t } from "../../../languageHandler";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
+export enum InfoTooltipKind {
+    Info = "info",
+    Warning = "warning",
+}
+
 interface ITooltipProps {
     tooltip?: React.ReactNode;
+    className?: string;
     tooltipClassName?: string;
+    kind?: InfoTooltipKind;
 }
 
 interface IState {
@@ -53,8 +60,12 @@ export default class InfoTooltip extends React.PureComponent<ITooltipProps, ISta
     };
 
     render() {
-        const { tooltip, children, tooltipClassName } = this.props;
+        const { tooltip, children, tooltipClassName, className, kind } = this.props;
         const title = _t("Information");
+        const iconClassName = (
+            (kind !== InfoTooltipKind.Warning) ?
+                "mx_InfoTooltip_icon_info" : "mx_InfoTooltip_icon_warning"
+        );
 
         // Tooltip are forced on the right for a more natural feel to them on info icons
         const tip = this.state.hover ? <Tooltip
@@ -64,10 +75,14 @@ export default class InfoTooltip extends React.PureComponent<ITooltipProps, ISta
             alignment={Alignment.Right}
         /> : <div />;
         return (
-            <div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">
-                <span className="mx_InfoTooltip_icon" aria-label={title} />
-                {children}
-                {tip}
+            <div
+                onMouseOver={this.onMouseOver}
+                onMouseLeave={this.onMouseLeave}
+                className={classNames("mx_InfoTooltip", className)}
+            >
+                <span className={classNames("mx_InfoTooltip_icon", iconClassName)} aria-label={title} />
+                { children }
+                { tip }
             </div>
         );
     }
diff --git a/src/components/views/elements/InlineSpinner.tsx b/src/components/views/elements/InlineSpinner.tsx
index a98e03502b..e2cda2f28d 100644
--- a/src/components/views/elements/InlineSpinner.tsx
+++ b/src/components/views/elements/InlineSpinner.tsx
@@ -39,7 +39,7 @@ export default class InlineSpinner extends React.PureComponent<IProps> {
                     style={{ width: this.props.w, height: this.props.h }}
                     aria-label={_t("Loading...")}
                 >
-                    {this.props.children}
+                    { this.props.children }
                 </div>
             </div>
         );
diff --git a/src/components/views/elements/InviteReason.tsx b/src/components/views/elements/InviteReason.tsx
index d684f61859..dff5c7d6bd 100644
--- a/src/components/views/elements/InviteReason.tsx
+++ b/src/components/views/elements/InviteReason.tsx
@@ -51,11 +51,11 @@ export default class InviteReason extends React.PureComponent<IProps, IState> {
         });
 
         return <div className={classes}>
-            <div className="mx_InviteReason_reason">{this.props.reason}</div>
+            <div className="mx_InviteReason_reason">{ this.props.reason }</div>
             <div className="mx_InviteReason_view"
                 onClick={this.onViewClick}
             >
-                {_t("View message")}
+                { _t("View message") }
             </div>
         </div>;
     }
diff --git a/src/components/views/elements/LabelledToggleSwitch.tsx b/src/components/views/elements/LabelledToggleSwitch.tsx
index 14853ea117..24647df502 100644
--- a/src/components/views/elements/LabelledToggleSwitch.tsx
+++ b/src/components/views/elements/LabelledToggleSwitch.tsx
@@ -57,8 +57,8 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
         const classes = `mx_SettingsFlag ${this.props.className || ""}`;
         return (
             <div className={classes}>
-                {firstPart}
-                {secondPart}
+                { firstPart }
+                { secondPart }
             </div>
         );
     }
diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js
index 22d4bfdd68..03aa9e0d6d 100644
--- a/src/components/views/elements/PersistedElement.js
+++ b/src/components/views/elements/PersistedElement.js
@@ -156,7 +156,7 @@ export default class PersistedElement extends React.Component {
     renderApp() {
         const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
             <div ref={this.collectChild} style={this.props.style}>
-                {this.props.children}
+                { this.props.children }
             </div>
         </MatrixClientContext.Provider>;
 
diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js
index ef449df295..016e7ddea5 100644
--- a/src/components/views/elements/PowerSelector.js
+++ b/src/components/views/elements/PowerSelector.js
@@ -161,7 +161,7 @@ export default class PowerSelector extends React.Component {
                     label={label} onChange={this.onSelectChange}
                     value={String(this.state.selectValue)} disabled={this.props.disabled}
                 >
-                    {options}
+                    { options }
                 </Field>
             );
         }
diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx
index 62de4dd2bb..a7676e4214 100644
--- a/src/components/views/elements/RoomAliasField.tsx
+++ b/src/components/views/elements/RoomAliasField.tsx
@@ -55,7 +55,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
     render() {
         const poundSign = (<span>#</span>);
         const aliasPostfix = ":" + this.props.domain;
-        const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
+        const domain = (<span title={aliasPostfix}>{ aliasPostfix }</span>);
         const maxlength = 255 - this.props.domain.length - 2;   // 2 for # and :
         return (
             <Field
diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx
index c2d1fcb275..5e6ef62504 100644
--- a/src/components/views/elements/ServerPicker.tsx
+++ b/src/components/views/elements/ServerPicker.tsx
@@ -63,28 +63,28 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
             });
         };
         editBtn = <AccessibleButton className="mx_ServerPicker_change" kind="link" onClick={onClick}>
-            {_t("Edit")}
+            { _t("Edit") }
         </AccessibleButton>;
     }
 
     let serverName: React.ReactNode = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl;
     if (serverConfig.hsNameIsDifferent) {
         serverName = <TextWithTooltip class="mx_Login_underlinedServerName" tooltip={serverConfig.hsUrl}>
-            {serverConfig.hsName}
+            { serverConfig.hsName }
         </TextWithTooltip>;
     }
 
     let desc;
     if (serverConfig.hsName === "matrix.org") {
         desc = <span className="mx_ServerPicker_desc">
-            {_t("Join millions for free on the largest public server")}
+            { _t("Join millions for free on the largest public server") }
         </span>;
     }
 
     return <div className="mx_ServerPicker">
-        <h3>{title || _t("Homeserver")}</h3>
+        <h3>{ title || _t("Homeserver") }</h3>
         <AccessibleButton className="mx_ServerPicker_help" onClick={onHelpClick} />
-        <span className="mx_ServerPicker_server">{serverName}</span>
+        <span className="mx_ServerPicker_server">{ serverName }</span>
         { editBtn }
         { desc }
     </div>;
diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx
index ccde80ff00..0847db801e 100644
--- a/src/components/views/elements/SettingsFlag.tsx
+++ b/src/components/views/elements/SettingsFlag.tsx
@@ -88,12 +88,12 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
                 onChange={this.checkBoxOnChange}
                 disabled={this.props.disabled || !canChange}
             >
-                {label}
+                { label }
             </StyledCheckbox>;
         } else {
             return (
                 <div className="mx_SettingsFlag">
-                    <span className="mx_SettingsFlag_label">{label}</span>
+                    <span className="mx_SettingsFlag_label">{ label }</span>
                     <ToggleSwitch
                         checked={this.state.value}
                         onChange={this.onChange}
diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx
index b9234d0550..df5776648e 100644
--- a/src/components/views/elements/Slider.tsx
+++ b/src/components/views/elements/Slider.tsx
@@ -98,7 +98,7 @@ export default class Slider extends React.Component<IProps> {
                     { selection }
                 </div>
                 <div className="mx_Slider_dotContainer">
-                    {dots}
+                    { dots }
                 </div>
             </div>
         </div>;
@@ -139,7 +139,7 @@ class Dot extends React.PureComponent<IDotProps> {
             <div className={className} />
             <div className="mx_Slider_labelContainer">
                 <div className="mx_Slider_label">
-                    {this.props.label}
+                    { this.props.label }
                 </div>
             </div>
         </span>;
diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
index 5230042c38..972dac909a 100644
--- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -45,7 +45,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
                                                                          SpellCheckLanguagesDropdownIState> {
     constructor(props) {
         super(props);
-        this._onSearchChange = this._onSearchChange.bind(this);
+        this.onSearchChange = this.onSearchChange.bind(this);
 
         this.state = {
             searchQuery: '',
@@ -76,10 +76,8 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
         }
     }
 
-    _onSearchChange(search) {
-        this.setState({
-            searchQuery: search,
-        });
+    private onSearchChange(searchQuery: string) {
+        this.setState({ searchQuery });
     }
 
     render() {
@@ -117,7 +115,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
             id="mx_LanguageDropdown"
             className={this.props.className}
             onOptionChange={this.props.onOptionChange}
-            onSearchChange={this._onSearchChange}
+            onSearchChange={this.onSearchChange}
             searchEnabled={true}
             value={value}
             label={_t("Language Dropdown")}>
diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.js
index 56c18c6e33..802c6cf841 100644
--- a/src/components/views/elements/Spoiler.js
+++ b/src/components/views/elements/Spoiler.js
@@ -37,7 +37,7 @@ export default class Spoiler extends React.Component {
 
     render() {
         const reason = this.props.reason ? (
-            <span className="mx_EventTile_spoiler_reason">{"(" + this.props.reason + ")"}</span>
+            <span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
         ) : null;
         // react doesn't allow appending a DOM node as child.
         // as such, we pass the this.props.contentHtml instead and then set the raw
diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx
index 366cc2f1f7..b609f7159e 100644
--- a/src/components/views/elements/StyledCheckbox.tsx
+++ b/src/components/views/elements/StyledCheckbox.tsx
@@ -44,7 +44,7 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
         return <span className={"mx_Checkbox " + className}>
             <input id={this.id} {...otherProps} type="checkbox" />
             <label htmlFor={this.id}>
-                {/* Using the div to center the image */}
+                { /* Using the div to center the image */ }
                 <div className="mx_Checkbox_background">
                     <img src={require("../../../../res/img/feather-customised/check.svg")} />
                 </div>
diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx
index 38d7358524..1b68274f39 100644
--- a/src/components/views/elements/StyledRadioButton.tsx
+++ b/src/components/views/elements/StyledRadioButton.tsx
@@ -50,22 +50,22 @@ export default class StyledRadioButton extends React.PureComponent<IProps, IStat
 
         const radioButton = <React.Fragment>
             <input type='radio' disabled={disabled} {...otherProps} />
-            {/* Used to render the radio button circle */}
+            { /* Used to render the radio button circle */ }
             <div><div /></div>
         </React.Fragment>;
 
         if (childrenInLabel) {
             return <label className={_className}>
-                {radioButton}
-                <div className="mx_RadioButton_content">{children}</div>
+                { radioButton }
+                <div className="mx_RadioButton_content">{ children }</div>
                 <div className="mx_RadioButton_spacer" />
             </label>;
         } else {
             return <div className={_className}>
                 <label className="mx_RadioButton_innerLabel">
-                    {radioButton}
+                    { radioButton }
                 </label>
-                <div className="mx_RadioButton_content">{children}</div>
+                <div className="mx_RadioButton_content">{ children }</div>
                 <div className="mx_RadioButton_spacer" />
             </div>;
         }
diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx
index 744b6f2059..1570304f93 100644
--- a/src/components/views/elements/StyledRadioGroup.tsx
+++ b/src/components/views/elements/StyledRadioGroup.tsx
@@ -52,7 +52,7 @@ function StyledRadioGroup<T extends string>({
     };
 
     return <React.Fragment>
-        {definitions.map(d => <React.Fragment key={d.value}>
+        { definitions.map(d => <React.Fragment key={d.value}>
             <StyledRadioButton
                 className={classNames(className, d.className)}
                 onChange={_onChange}
@@ -65,7 +65,7 @@ function StyledRadioGroup<T extends string>({
                 { d.label }
             </StyledRadioButton>
             { d.description ? <span>{ d.description }</span> : null }
-        </React.Fragment>)}
+        </React.Fragment>) }
     </React.Fragment>;
 }
 
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index 12c3718274..14a5ed8e6b 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -152,7 +152,7 @@ export default class TagTile extends React.Component {
                 "mx_TagTile_badge": true,
                 "mx_TagTile_badgeHighlight": badge.highlight,
             });
-            badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>);
+            badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badge.count) }</div>);
         }
 
         const contextButton = this.state.hover || this.props.menuDisplayed ?
@@ -161,7 +161,7 @@ export default class TagTile extends React.Component {
                 onClick={this.openMenu}
                 inputRef={this.props.contextMenuButtonRef}
             >
-                {"\u00B7\u00B7\u00B7"}
+                { "\u00B7\u00B7\u00B7" }
             </AccessibleButton> : <div ref={this.props.contextMenuButtonRef} />;
 
         const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
@@ -184,8 +184,8 @@ export default class TagTile extends React.Component {
                     width={avatarSize}
                     height={avatarSize}
                 />
-                {contextButton}
-                {badgeElement}
+                { contextButton }
+                { badgeElement }
             </div>
         </AccessibleTooltipButton>;
     }
diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js
index 633d182fcf..288d33f71b 100644
--- a/src/components/views/elements/TextWithTooltip.js
+++ b/src/components/views/elements/TextWithTooltip.js
@@ -51,12 +51,12 @@ export default class TextWithTooltip extends React.Component {
 
         return (
             <span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
-                {children}
-                {this.state.hover && <Tooltip
+                { children }
+                { this.state.hover && <Tooltip
                     {...tooltipProps}
                     label={tooltip}
                     tooltipClassName={tooltipClass}
-                    className={"mx_TextWithTooltip_tooltip"}
+                    className="mx_TextWithTooltip_tooltip"
                 /> }
             </span>
         );
diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx
index ad3571513c..f887741ff7 100644
--- a/src/components/views/elements/Validation.tsx
+++ b/src/components/views/elements/Validation.tsx
@@ -147,16 +147,16 @@ export default function withValidation<T = undefined, D = void>({
         let details;
         if (results && results.length) {
             details = <ul className="mx_Validation_details">
-                {results.map(result => {
+                { results.map(result => {
                     const classes = classNames({
                         "mx_Validation_detail": true,
                         "mx_Validation_valid": result.valid,
                         "mx_Validation_invalid": !result.valid,
                     });
                     return <li key={result.key} className={classes}>
-                        {result.text}
+                        { result.text }
                     </li>;
-                })}
+                }) }
             </ul>;
         }
 
@@ -165,14 +165,14 @@ export default function withValidation<T = undefined, D = void>({
             // We're setting `this` to whichever component holds the validation
             // function. That allows rules to access the state of the component.
             const content = description.call(this, derivedData);
-            summary = <div className="mx_Validation_description">{content}</div>;
+            summary = <div className="mx_Validation_description">{ content }</div>;
         }
 
         let feedback;
         if (summary || details) {
             feedback = <div className="mx_Validation">
-                {summary}
-                {details}
+                { summary }
+                { details }
             </div>;
         }
 
diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx
index 24b4fbe3ed..244de96243 100644
--- a/src/components/views/emojipicker/Category.tsx
+++ b/src/components/views/emojipicker/Category.tsx
@@ -98,7 +98,7 @@ class Category extends React.PureComponent<IProps> {
                 aria-label={name}
             >
                 <h2 className="mx_EmojiPicker_category_label">
-                    {name}
+                    { name }
                 </h2>
                 <LazyRenderList
                     element="ul" className="mx_EmojiPicker_list"
diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx
index 73e24f46fb..48194ff7d7 100644
--- a/src/components/views/emojipicker/Emoji.tsx
+++ b/src/components/views/emojipicker/Emoji.tsx
@@ -44,7 +44,7 @@ class Emoji extends React.PureComponent<IProps> {
                 label={emoji.unicode}
             >
                 <div className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}>
-                    {emoji.unicode}
+                    { emoji.unicode }
                 </div>
             </MenuItem>
         );
diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx
index 9b2e771e64..0884db7101 100644
--- a/src/components/views/emojipicker/EmojiPicker.tsx
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -32,6 +32,8 @@ export const CATEGORY_HEADER_HEIGHT = 22;
 export const EMOJI_HEIGHT = 37;
 export const EMOJIS_PER_ROW = 8;
 
+const ZERO_WIDTH_JOINER = "\u200D";
+
 interface IProps {
     selectedEmojis?: Set<string>;
     showQuickReactions?: boolean;
@@ -180,7 +182,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
             } else {
                 emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id];
             }
-            emojis = emojis.filter(emoji => emoji.filterString.includes(filter));
+            emojis = emojis.filter(emoji => this.emojiMatchesFilter(emoji, filter));
             this.memoizedDataByCategory[cat.id] = emojis;
             cat.enabled = emojis.length > 0;
             // The setState below doesn't re-render the header and we already have the refs for updateVisibility, so...
@@ -192,6 +194,10 @@ class EmojiPicker extends React.Component<IProps, IState> {
         setTimeout(this.updateVisibility, 0);
     };
 
+    private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean =>
+        [emoji.annotation, ...emoji.shortcodes, emoji.emoticon, ...emoji.unicode.split(ZERO_WIDTH_JOINER)]
+            .some(x => x?.includes(filter));
+
     private onEnterFilter = () => {
         const btn = this.bodyRef.current.querySelector<HTMLButtonElement>(".mx_EmojiPicker_item");
         if (btn) {
@@ -238,7 +244,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
                     }}
                     onScroll={this.onScroll}
                 >
-                    {this.categories.map(category => {
+                    { this.categories.map(category => {
                         const emojis = this.memoizedDataByCategory[category.id];
                         const categoryElement = ((
                             <Category
@@ -258,9 +264,9 @@ class EmojiPicker extends React.Component<IProps, IState> {
                         const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length);
                         heightBefore += height;
                         return categoryElement;
-                    })}
+                    }) }
                 </AutoHideScrollbar>
-                {this.state.previewEmoji || !this.props.showQuickReactions
+                { this.state.previewEmoji || !this.props.showQuickReactions
                     ? <Preview emoji={this.state.previewEmoji} />
                     : <QuickReactions onClick={this.onClickEmoji} selectedEmojis={this.props.selectedEmojis} /> }
             </div>
diff --git a/src/components/views/emojipicker/Header.tsx b/src/components/views/emojipicker/Header.tsx
index ac39affdd9..e4619dedf2 100644
--- a/src/components/views/emojipicker/Header.tsx
+++ b/src/components/views/emojipicker/Header.tsx
@@ -89,7 +89,7 @@ class Header extends React.PureComponent<IProps> {
                 aria-label={_t("Categories")}
                 onKeyDown={this.onKeyDown}
             >
-                {this.props.categories.map(category => {
+                { this.props.categories.map(category => {
                     const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
                         mx_EmojiPicker_anchor_visible: category.visible,
                     });
@@ -106,7 +106,7 @@ class Header extends React.PureComponent<IProps> {
                         aria-selected={category.visible}
                         aria-controls={`mx_EmojiPicker_category_${category.id}`}
                     />;
-                })}
+                }) }
             </nav>
         );
     }
diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx
index 9c2dbb9cbd..02b5669caf 100644
--- a/src/components/views/emojipicker/Preview.tsx
+++ b/src/components/views/emojipicker/Preview.tsx
@@ -27,23 +27,19 @@ interface IProps {
 @replaceableComponent("views.emojipicker.Preview")
 class Preview extends React.PureComponent<IProps> {
     render() {
-        const {
-            unicode = "",
-            annotation = "",
-            shortcodes: [shortcode = ""],
-        } = this.props.emoji || {};
+        const { unicode, annotation, shortcodes: [shortcode] } = this.props.emoji;
 
         return (
             <div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">
                 <div className="mx_EmojiPicker_preview_emoji">
-                    {unicode}
+                    { unicode }
                 </div>
                 <div className="mx_EmojiPicker_preview_text">
                     <div className="mx_EmojiPicker_name mx_EmojiPicker_preview_name">
-                        {annotation}
+                        { annotation }
                     </div>
                     <div className="mx_EmojiPicker_shortcode">
-                        {shortcode}
+                        { shortcode }
                     </div>
                 </div>
             </div>
diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx
index ffd3ce9760..cbd529a43c 100644
--- a/src/components/views/emojipicker/QuickReactions.tsx
+++ b/src/components/views/emojipicker/QuickReactions.tsx
@@ -65,16 +65,16 @@ class QuickReactions extends React.Component<IProps, IState> {
         return (
             <section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category">
                 <h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label">
-                    {!this.state.hover
+                    { !this.state.hover
                         ? _t("Quick Reactions")
                         : <React.Fragment>
-                            <span className="mx_EmojiPicker_name">{this.state.hover.annotation}</span>
-                            <span className="mx_EmojiPicker_shortcode">{this.state.hover.shortcodes[0]}</span>
+                            <span className="mx_EmojiPicker_name">{ this.state.hover.annotation }</span>
+                            <span className="mx_EmojiPicker_shortcode">{ this.state.hover.shortcodes[0] }</span>
                         </React.Fragment>
                     }
                 </h2>
                 <ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
-                    {QUICK_REACTIONS.map(emoji => ((
+                    { QUICK_REACTIONS.map(emoji => ((
                         <Emoji
                             key={emoji.hexcode}
                             emoji={emoji}
@@ -83,7 +83,7 @@ class QuickReactions extends React.Component<IProps, IState> {
                             onMouseLeave={this.onMouseLeave}
                             selectedEmojis={this.props.selectedEmojis}
                         />
-                    )))}
+                    ))) }
                 </ul>
             </section>
         );
diff --git a/src/components/views/emojipicker/Search.tsx b/src/components/views/emojipicker/Search.tsx
index c88bf8d84d..6ee8907373 100644
--- a/src/components/views/emojipicker/Search.tsx
+++ b/src/components/views/emojipicker/Search.tsx
@@ -69,7 +69,7 @@ class Search extends React.PureComponent<IProps> {
                     onKeyDown={this.onKeyDown}
                     ref={this.inputRef}
                 />
-                {rightButton}
+                { rightButton }
             </div>
         );
     }
diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js
index c12e14e024..0ec0084162 100644
--- a/src/components/views/groups/GroupInviteTile.js
+++ b/src/components/views/groups/GroupInviteTile.js
@@ -165,7 +165,7 @@ export default class GroupInviteTile extends React.Component {
 
         return <React.Fragment>
             <RovingTabIndexWrapper>
-                {({ onFocus, isActive, ref }) =>
+                { ({ onFocus, isActive, ref }) =>
                     <AccessibleButton
                         onFocus={onFocus}
                         tabIndex={isActive ? 0 : -1}
diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js
index 5d892cdb76..f6b01d7d64 100644
--- a/src/components/views/groups/GroupMemberList.js
+++ b/src/components/views/groups/GroupMemberList.js
@@ -201,7 +201,7 @@ export default class GroupMemberList extends React.Component {
 
         const invited = (this.state.invitedMembers && this.state.invitedMembers.length > 0) ?
             <div className="mx_MemberList_invited">
-                <h2>{_t("Invited")}</h2>
+                <h2>{ _t("Invited") }</h2>
                 {
                     this.makeGroupMemberTiles(
                         this.state.searchQuery,
diff --git a/src/components/views/host_signup/HostSignupContainer.tsx b/src/components/views/host_signup/HostSignupContainer.tsx
index fc1506bf61..1b9f0c1e45 100644
--- a/src/components/views/host_signup/HostSignupContainer.tsx
+++ b/src/components/views/host_signup/HostSignupContainer.tsx
@@ -27,7 +27,7 @@ const HostSignupContainer = () => {
     });
 
     return <div className="mx_HostSignupContainer">
-        {isActive &&
+        { isActive &&
             <HostSignupDialog />
         }
     </div>;
diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx
new file mode 100644
index 0000000000..c0be3b46bb
--- /dev/null
+++ b/src/components/views/messages/CallEvent.tsx
@@ -0,0 +1,218 @@
+/*
+Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { _t, _td } from '../../../languageHandler';
+import MemberAvatar from '../avatars/MemberAvatar';
+import CallEventGrouper, { CallEventGrouperEvent, CustomCallState } from '../../structures/CallEventGrouper';
+import AccessibleButton from '../elements/AccessibleButton';
+import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
+import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
+import classNames from 'classnames';
+import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
+
+interface IProps {
+    mxEvent: MatrixEvent;
+    callEventGrouper: CallEventGrouper;
+}
+
+interface IState {
+    callState: CallState | CustomCallState;
+    silenced: boolean;
+}
+
+const TEXTUAL_STATES: Map<CallState | CustomCallState, string> = new Map([
+    [CallState.Connected, _td("Connected")],
+    [CallState.Connecting, _td("Connecting")],
+]);
+
+export default class CallEvent extends React.Component<IProps, IState> {
+    constructor(props: IProps) {
+        super(props);
+
+        this.state = {
+            callState: this.props.callEventGrouper.state,
+            silenced: false,
+        };
+    }
+
+    componentDidMount() {
+        this.props.callEventGrouper.addListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
+        this.props.callEventGrouper.addListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
+    }
+
+    componentWillUnmount() {
+        this.props.callEventGrouper.removeListener(CallEventGrouperEvent.StateChanged, this.onStateChanged);
+        this.props.callEventGrouper.removeListener(CallEventGrouperEvent.SilencedChanged, this.onSilencedChanged);
+    }
+
+    private onSilencedChanged = (newState) => {
+        this.setState({ silenced: newState });
+    };
+
+    private onStateChanged = (newState: CallState) => {
+        this.setState({ callState: newState });
+    };
+
+    private renderContent(state: CallState | CustomCallState): JSX.Element {
+        if (state === CallState.Ringing) {
+            const silenceClass = classNames({
+                "mx_CallEvent_iconButton": true,
+                "mx_CallEvent_unSilence": this.state.silenced,
+                "mx_CallEvent_silence": !this.state.silenced,
+            });
+
+            return (
+                <div className="mx_CallEvent_content">
+                    <AccessibleTooltipButton
+                        className={silenceClass}
+                        onClick={this.props.callEventGrouper.toggleSilenced}
+                        title={this.state.silenced ? _t("Sound on"): _t("Silence call")}
+                    />
+                    <AccessibleButton
+                        className="mx_CallEvent_content_button mx_CallEvent_content_button_reject"
+                        onClick={this.props.callEventGrouper.rejectCall}
+                        kind="danger"
+                    >
+                        <span> { _t("Decline") } </span>
+                    </AccessibleButton>
+                    <AccessibleButton
+                        className="mx_CallEvent_content_button mx_CallEvent_content_button_answer"
+                        onClick={this.props.callEventGrouper.answerCall}
+                        kind="primary"
+                    >
+                        <span> { _t("Accept") } </span>
+                    </AccessibleButton>
+                </div>
+            );
+        }
+        if (state === CallState.Ended) {
+            const hangupReason = this.props.callEventGrouper.hangupReason;
+
+            if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) {
+                // 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)
+                // Also, if we don't have a reason
+                return (
+                    <div className="mx_CallEvent_content">
+                        { _t("This call has ended") }
+                    </div>
+                );
+            }
+
+            let reason;
+            if (hangupReason === CallErrorCode.IceFailed) {
+                // We couldn't establish a connection at all
+                reason = _t("Could not connect media");
+            } else if (hangupReason === "ice_timeout") {
+                // We established a connection but it died
+                reason = _t("Connection failed");
+            } else if (hangupReason === CallErrorCode.NoUserMedia) {
+                // The other side couldn't open capture devices
+                reason = _t("Their device couldn't start the camera or microphone");
+            } else if (hangupReason === "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 unknown error occurred");
+            } else if (hangupReason === CallErrorCode.InviteTimeout) {
+                reason = _t("No answer");
+            } else if (hangupReason === CallErrorCode.UserBusy) {
+                reason = _t("The user you called is busy.");
+            } else {
+                reason = _t('Unknown failure: %(reason)s)', { reason: hangupReason });
+            }
+
+            return (
+                <div className="mx_CallEvent_content">
+                    <InfoTooltip
+                        tooltip={reason}
+                        className="mx_CallEvent_content_tooltip"
+                        kind={InfoTooltipKind.Warning}
+                    />
+                    { _t("This call has failed") }
+                </div>
+            );
+        }
+        if (Array.from(TEXTUAL_STATES.keys()).includes(state)) {
+            return (
+                <div className="mx_CallEvent_content">
+                    { TEXTUAL_STATES.get(state) }
+                </div>
+            );
+        }
+        if (state === CustomCallState.Missed) {
+            return (
+                <div className="mx_CallEvent_content">
+                    { _t("You missed this call") }
+                    <AccessibleButton
+                        className="mx_CallEvent_content_button mx_CallEvent_content_button_callBack"
+                        onClick={this.props.callEventGrouper.callBack}
+                        kind="primary"
+                    >
+                        <span> { _t("Call back") } </span>
+                    </AccessibleButton>
+                </div>
+            );
+        }
+
+        return (
+            <div className="mx_CallEvent_content">
+                { _t("The call is in an unknown state!") }
+            </div>
+        );
+    }
+
+    render() {
+        const event = this.props.mxEvent;
+        const sender = event.sender ? event.sender.name : event.getSender();
+        const isVoice = this.props.callEventGrouper.isVoice;
+        const callType = isVoice ? _t("Voice call") : _t("Video call");
+        const content = this.renderContent(this.state.callState);
+        const className = classNames({
+            mx_CallEvent: true,
+            mx_CallEvent_voice: isVoice,
+            mx_CallEvent_video: !isVoice,
+        });
+
+        return (
+            <div className={className}>
+                <div className="mx_CallEvent_info">
+                    <MemberAvatar
+                        member={event.sender}
+                        width={32}
+                        height={32}
+                    />
+                    <div className="mx_CallEvent_info_basic">
+                        <div className="mx_CallEvent_sender">
+                            { sender }
+                        </div>
+                        <div className="mx_CallEvent_type">
+                            <div className="mx_CallEvent_type_icon"></div>
+                            { callType }
+                        </div>
+                    </div>
+                </div>
+                { content }
+            </div>
+        );
+    }
+}
diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx
new file mode 100644
index 0000000000..2bdae04eda
--- /dev/null
+++ b/src/components/views/messages/DownloadActionButton.tsx
@@ -0,0 +1,109 @@
+/*
+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 { MatrixEvent } from "matrix-js-sdk/src";
+import { MediaEventHelper } from "../../../utils/MediaEventHelper";
+import React, { createRef } from "react";
+import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
+import Spinner from "../elements/Spinner";
+import classNames from "classnames";
+import { _t } from "../../../languageHandler";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+    mxEvent: MatrixEvent;
+
+    // XXX: It can take a cycle or two for the MessageActionBar to have all the props/setup
+    // required to get us a MediaEventHelper, so we use a getter function instead to prod for
+    // one.
+    mediaEventHelperGet: () => MediaEventHelper;
+}
+
+interface IState {
+    loading: boolean;
+    blob?: Blob;
+}
+
+@replaceableComponent("views.messages.DownloadActionButton")
+export default class DownloadActionButton extends React.PureComponent<IProps, IState> {
+    private iframe: React.RefObject<HTMLIFrameElement> = createRef();
+
+    public constructor(props: IProps) {
+        super(props);
+
+        this.state = {
+            loading: false,
+        };
+    }
+
+    private onDownloadClick = async () => {
+        if (this.state.loading) return;
+
+        this.setState({ loading: true });
+
+        if (this.state.blob) {
+            // Cheat and trigger a download, again.
+            return this.onFrameLoad();
+        }
+
+        const blob = await this.props.mediaEventHelperGet().sourceBlob.value;
+        this.setState({ blob });
+    };
+
+    private onFrameLoad = () => {
+        this.setState({ loading: false });
+
+        // we aren't showing the iframe, so we can send over the bare minimum styles and such.
+        this.iframe.current.contentWindow.postMessage({
+            imgSrc: "", // no image
+            imgStyle: null,
+            style: "",
+            blob: this.state.blob,
+            download: this.props.mediaEventHelperGet().fileName,
+            textContent: "",
+            auto: true, // autodownload
+        }, '*');
+    };
+
+    public render() {
+        let spinner: JSX.Element;
+        if (this.state.loading) {
+            spinner = <Spinner w={18} h={18} />;
+        }
+
+        const classes = classNames({
+            'mx_MessageActionBar_maskButton': true,
+            'mx_MessageActionBar_downloadButton': true,
+            'mx_MessageActionBar_downloadSpinnerButton': !!spinner,
+        });
+
+        return <RovingAccessibleTooltipButton
+            className={classes}
+            title={spinner ? _t("Downloading") : _t("Download")}
+            onClick={this.onDownloadClick}
+            disabled={!!spinner}
+        >
+            { spinner }
+            { this.state.blob && <iframe
+                src="usercontent/" // XXX: Like MFileBody, this should come from the skin
+                ref={this.iframe}
+                onLoad={this.onFrameLoad}
+                sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation"
+                style={{ display: "none" }}
+            /> }
+        </RovingAccessibleTooltipButton>;
+    }
+}
diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js
index 1416cfff53..2c6a567f6b 100644
--- a/src/components/views/messages/EditHistoryMessage.js
+++ b/src/components/views/messages/EditHistoryMessage.js
@@ -110,20 +110,20 @@ export default class EditHistoryMessage extends React.PureComponent {
         if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) {
             redactButton = (
                 <AccessibleButton onClick={this._onRedactClick}>
-                    {_t("Remove")}
+                    { _t("Remove") }
                 </AccessibleButton>
             );
         }
         const viewSourceButton = (
             <AccessibleButton onClick={this._onViewSourceClick}>
-                {_t("View Source")}
+                { _t("View Source") }
             </AccessibleButton>
         );
         // disabled remove button when not allowed
         return (
             <div className="mx_MessageActionBar">
-                {redactButton}
-                {viewSourceButton}
+                { redactButton }
+                { viewSourceButton }
             </div>
         );
     }
@@ -146,11 +146,11 @@ export default class EditHistoryMessage extends React.PureComponent {
                 contentContainer = (
                     <div className="mx_EventTile_content" ref={this._content}>*&nbsp;
                         <span className="mx_MEmoteBody_sender">{ name }</span>
-                        &nbsp;{contentElements}
+                        &nbsp;{ contentElements }
                     </div>
                 );
             } else {
-                contentContainer = <div className="mx_EventTile_content" ref={this._content}>{contentElements}</div>;
+                contentContainer = <div className="mx_EventTile_content" ref={this._content}>{ contentElements }</div>;
             }
         }
 
@@ -165,7 +165,7 @@ export default class EditHistoryMessage extends React.PureComponent {
             <li>
                 <div className={classes}>
                     <div className="mx_EventTile_line">
-                        <span className="mx_MessageTimestamp">{timestamp}</span>
+                        <span className="mx_MessageTimestamp">{ timestamp }</span>
                         { contentContainer }
                         { this._renderActionBar() }
                     </div>
diff --git a/src/components/views/messages/IBodyProps.ts b/src/components/views/messages/IBodyProps.ts
new file mode 100644
index 0000000000..8aabd3080c
--- /dev/null
+++ b/src/components/views/messages/IBodyProps.ts
@@ -0,0 +1,43 @@
+/*
+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 { MatrixEvent } from "matrix-js-sdk/src";
+import { TileShape } from "../rooms/EventTile";
+import { MediaEventHelper } from "../../../utils/MediaEventHelper";
+import EditorStateTransfer from "../../../utils/EditorStateTransfer";
+import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
+
+export interface IBodyProps {
+    mxEvent: MatrixEvent;
+
+    /* a list of words to highlight */
+    highlights: string[];
+
+    /* link URL for the highlights */
+    highlightLink: string;
+
+    /* callback called when dynamic content in events are loaded */
+    onHeightChanged: () => void;
+
+    showUrlPreview?: boolean;
+    tileShape: TileShape;
+    maxImageHeight?: number;
+    replacingEventId?: string;
+    editState?: EditorStateTransfer;
+    onMessageAllowed: () => void; // TODO: Docs
+    permalinkCreator: RoomPermalinkCreator;
+    mediaEventHelper: MediaEventHelper;
+}
diff --git a/src/components/views/messages/IMediaBody.ts b/src/components/views/messages/IMediaBody.ts
new file mode 100644
index 0000000000..27b5f24275
--- /dev/null
+++ b/src/components/views/messages/IMediaBody.ts
@@ -0,0 +1,21 @@
+/*
+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 { MediaEventHelper } from "../../../utils/MediaEventHelper";
+
+export interface IMediaBody {
+    getMediaHelper(): MediaEventHelper;
+}
diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx
index bc7216f42c..3444c2a3d0 100644
--- a/src/components/views/messages/MAudioBody.tsx
+++ b/src/components/views/messages/MAudioBody.tsx
@@ -15,30 +15,23 @@ limitations under the License.
 */
 
 import React from "react";
-import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import { Playback } from "../../../voice/Playback";
-import MFileBody from "./MFileBody";
 import InlineSpinner from '../elements/InlineSpinner';
 import { _t } from "../../../languageHandler";
-import { mediaFromContent } from "../../../customisations/Media";
-import { decryptFile } from "../../../utils/DecryptFile";
-import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
 import AudioPlayer from "../audio_messages/AudioPlayer";
-
-interface IProps {
-    mxEvent: MatrixEvent;
-}
+import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
+import MFileBody from "./MFileBody";
+import { IBodyProps } from "./IBodyProps";
 
 interface IState {
     error?: Error;
     playback?: Playback;
-    decryptedBlob?: Blob;
 }
 
 @replaceableComponent("views.messages.MAudioBody")
-export default class MAudioBody extends React.PureComponent<IProps, IState> {
-    constructor(props: IProps) {
+export default class MAudioBody extends React.PureComponent<IBodyProps, IState> {
+    constructor(props: IBodyProps) {
         super(props);
 
         this.state = {};
@@ -46,33 +39,34 @@ export default class MAudioBody extends React.PureComponent<IProps, IState> {
 
     public async componentDidMount() {
         let buffer: ArrayBuffer;
-        const content: IMediaEventContent = this.props.mxEvent.getContent();
-        const media = mediaFromContent(content);
-        if (media.isEncrypted) {
+
+        try {
             try {
-                const blob = await decryptFile(content.file);
+                const blob = await this.props.mediaEventHelper.sourceBlob.value;
                 buffer = await blob.arrayBuffer();
-                this.setState({ decryptedBlob: blob });
             } catch (e) {
                 this.setState({ error: e });
                 console.warn("Unable to decrypt audio message", e);
                 return; // stop processing the audio file
             }
-        } else {
-            try {
-                buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer());
-            } catch (e) {
-                this.setState({ error: e });
-                console.warn("Unable to download audio message", e);
-                return; // stop processing the audio file
-            }
+        } catch (e) {
+            this.setState({ error: e });
+            console.warn("Unable to decrypt/download audio message", e);
+            return; // stop processing the audio file
         }
 
         // We should have a buffer to work with now: let's set it up
-        const playback = new Playback(buffer);
+
+        // Note: we don't actually need a waveform to render an audio event, but voice messages do.
+        const content = this.props.mxEvent.getContent<IMediaEventContent>();
+        const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024);
+
+        // We should have a buffer to work with now: let's set it up
+        const playback = new Playback(buffer, waveform);
         playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent);
         this.setState({ playback });
-        // Note: the RecordingPlayback component will handle preparing the Playback class for us.
+
+        // Note: the components later on will handle preparing the Playback class for us.
     }
 
     public componentWillUnmount() {
@@ -103,7 +97,7 @@ export default class MAudioBody extends React.PureComponent<IProps, IState> {
         return (
             <span className="mx_MAudioBody">
                 <AudioPlayer playback={this.state.playback} mediaName={this.props.mxEvent.getContent().body} />
-                <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
+                { this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
             </span>
         );
     }
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.tsx
similarity index 73%
rename from src/components/views/messages/MFileBody.js
rename to src/components/views/messages/MFileBody.tsx
index 9236c77e8d..b1e42976db 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.tsx
@@ -15,26 +15,29 @@ limitations under the License.
 */
 
 import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
 import filesize from 'filesize';
 import { _t } from '../../../languageHandler';
-import { decryptFile } from '../../../utils/DecryptFile';
 import Modal from '../../../Modal';
 import AccessibleButton from "../elements/AccessibleButton";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import { mediaFromContent } from "../../../customisations/Media";
 import ErrorDialog from "../dialogs/ErrorDialog";
 import { TileShape } from "../rooms/EventTile";
+import { IContent } from "matrix-js-sdk/src";
+import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
+import { IBodyProps } from "./IBodyProps";
 
-let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on
+export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on
 
 async function cacheDownloadIcon() {
-    if (downloadIconUrl) return; // cached already
+    if (DOWNLOAD_ICON_URL) return; // cached already
+    // eslint-disable-next-line @typescript-eslint/no-var-requires
     const svg = await fetch(require("../../../../res/img/download.svg")).then(r => r.text());
-    downloadIconUrl = "data:image/svg+xml;base64," + window.btoa(svg);
+    DOWNLOAD_ICON_URL = "data:image/svg+xml;base64," + window.btoa(svg);
 }
 
 // Cache the asset immediately
+// noinspection JSIgnoredPromiseFromCall
 cacheDownloadIcon();
 
 // User supplied content can contain scripts, we have to be careful that
@@ -72,7 +75,7 @@ cacheDownloadIcon();
  * @param {HTMLElement} element The element to get the current style of.
  * @return {string} The CSS style encoded as a string.
  */
-function computedStyle(element) {
+export function computedStyle(element: HTMLElement) {
     if (!element) {
         return "";
     }
@@ -98,7 +101,7 @@ function computedStyle(element) {
  * @param {boolean} withSize Whether to include size information. Default true.
  * @return {string} the human readable link text for the attachment.
  */
-export function presentableTextForFile(content, withSize = true) {
+export function presentableTextForFile(content: IContent, withSize = true): string {
     let linkText = _t("Attachment");
     if (content.body && content.body.length > 0) {
         // The content body should be the name of the file including a
@@ -119,53 +122,48 @@ export function presentableTextForFile(content, withSize = true) {
     return linkText;
 }
 
-@replaceableComponent("views.messages.MFileBody")
-export default class MFileBody extends React.Component {
-    static propTypes = {
-        /* the MatrixEvent to show */
-        mxEvent: PropTypes.object.isRequired,
-        /* already decrypted blob */
-        decryptedBlob: PropTypes.object,
-        /* called when the download link iframe is shown */
-        onHeightChanged: PropTypes.func,
-        /* the shape of the tile, used */
-        tileShape: PropTypes.string,
-        /* whether or not to show the default placeholder for the file. Defaults to true. */
-        showGenericPlaceholder: PropTypes.bool,
-    };
+interface IProps extends IBodyProps {
+    /* whether or not to show the default placeholder for the file. Defaults to true. */
+    showGenericPlaceholder: boolean;
+}
 
+interface IState {
+    decryptedBlob?: Blob;
+}
+
+@replaceableComponent("views.messages.MFileBody")
+export default class MFileBody extends React.Component<IProps, IState> {
     static defaultProps = {
         showGenericPlaceholder: true,
     };
 
-    constructor(props) {
+    private iframe: React.RefObject<HTMLIFrameElement> = createRef();
+    private dummyLink: React.RefObject<HTMLAnchorElement> = createRef();
+    private userDidClick = false;
+
+    public constructor(props: IProps) {
         super(props);
 
-        this.state = {
-            decryptedBlob: (this.props.decryptedBlob ? this.props.decryptedBlob : null),
-        };
-
-        this._iframe = createRef();
-        this._dummyLink = createRef();
+        this.state = {};
     }
 
-    _getContentUrl() {
+    private getContentUrl(): string {
         const media = mediaFromContent(this.props.mxEvent.getContent());
         return media.srcHttp;
     }
 
-    componentDidUpdate(prevProps, prevState) {
+    public componentDidUpdate(prevProps, prevState) {
         if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
             this.props.onHeightChanged();
         }
     }
 
-    render() {
-        const content = this.props.mxEvent.getContent();
+    public render() {
+        const content = this.props.mxEvent.getContent<IMediaEventContent>();
         const text = presentableTextForFile(content);
-        const isEncrypted = content.file !== undefined;
+        const isEncrypted = this.props.mediaEventHelper.media.isEncrypted;
         const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
-        const contentUrl = this._getContentUrl();
+        const contentUrl = this.getContentUrl();
         const fileSize = content.info ? content.info.size : null;
         const fileType = content.info ? content.info.mimetype : "application/octet-stream";
 
@@ -181,42 +179,38 @@ export default class MFileBody extends React.Component {
             );
         }
 
+        const showDownloadLink = this.props.tileShape || !this.props.showGenericPlaceholder;
+
         if (isEncrypted) {
-            if (this.state.decryptedBlob === null) {
+            if (!this.state.decryptedBlob) {
                 // Need to decrypt the attachment
                 // Wait for the user to click on the link before downloading
                 // and decrypting the attachment.
-                let decrypting = false;
-                const decrypt = (e) => {
-                    if (decrypting) {
-                        return false;
-                    }
-                    decrypting = true;
-                    decryptFile(content.file).then((blob) => {
+                const decrypt = async () => {
+                    try {
+                        this.userDidClick = true;
                         this.setState({
-                            decryptedBlob: blob,
+                            decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
                         });
-                    }).catch((err) => {
+                    } catch (err) {
                         console.warn("Unable to decrypt attachment: ", err);
                         Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, {
                             title: _t("Error"),
                             description: _t("Error decrypting attachment"),
                         });
-                    }).finally(() => {
-                        decrypting = false;
-                    });
+                    }
                 };
 
                 // This button should actually Download because usercontent/ will try to click itself
                 // but it is not guaranteed between various browsers' settings.
                 return (
                     <span className="mx_MFileBody">
-                        {placeholder}
-                        <div className="mx_MFileBody_download">
+                        { placeholder }
+                        { showDownloadLink && <div className="mx_MFileBody_download">
                             <AccessibleButton onClick={decrypt}>
                                 { _t("Decrypt %(text)s", { text: text }) }
                             </AccessibleButton>
-                        </div>
+                        </div> }
                     </span>
                 );
             }
@@ -224,9 +218,9 @@ export default class MFileBody extends React.Component {
             // When the iframe loads we tell it to render a download link
             const onIframeLoad = (ev) => {
                 ev.target.contentWindow.postMessage({
-                    imgSrc: downloadIconUrl,
+                    imgSrc: DOWNLOAD_ICON_URL,
                     imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon.
-                    style: computedStyle(this._dummyLink.current),
+                    style: computedStyle(this.dummyLink.current),
                     blob: this.state.decryptedBlob,
                     // Set a download attribute for encrypted files so that the file
                     // will have the correct name when the user tries to download it.
@@ -234,7 +228,7 @@ export default class MFileBody extends React.Component {
                     download: fileName,
                     textContent: _t("Download %(text)s", { text: text }),
                     // only auto-download if a user triggered this iframe explicitly
-                    auto: !this.props.decryptedBlob,
+                    auto: this.userDidClick,
                 }, "*");
             };
 
@@ -243,22 +237,22 @@ export default class MFileBody extends React.Component {
             // If the attachment is encrypted then put the link inside an iframe.
             return (
                 <span className="mx_MFileBody">
-                    {placeholder}
-                    <div className="mx_MFileBody_download">
+                    { placeholder }
+                    { showDownloadLink && <div className="mx_MFileBody_download">
                         <div style={{ display: "none" }}>
                             { /*
                               * Add dummy copy of the "a" tag
                               * We'll use it to learn how the download link
                               * would have been styled if it was rendered inline.
                               */ }
-                            <a ref={this._dummyLink} />
+                            <a ref={this.dummyLink} />
                         </div>
                         <iframe
                             src={url}
                             onLoad={onIframeLoad}
-                            ref={this._iframe}
+                            ref={this.iframe}
                             sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
-                    </div>
+                    </div> }
                 </span>
             );
         } else if (contentUrl) {
@@ -289,7 +283,7 @@ export default class MFileBody extends React.Component {
 
                     // Start a fetch for the download
                     // Based upon https://stackoverflow.com/a/49500465
-                    fetch(contentUrl).then((response) => response.blob()).then((blob) => {
+                    this.props.mediaEventHelper.sourceBlob.value.then((blob) => {
                         const blobUrl = URL.createObjectURL(blob);
 
                         // We have to create an anchor to download the file
@@ -306,40 +300,24 @@ export default class MFileBody extends React.Component {
                 downloadProps["download"] = fileName;
             }
 
-            // If the attachment is not encrypted then we check whether we
-            // are being displayed in the room timeline or in a list of
-            // files in the right hand side of the screen.
-            if (this.props.tileShape === TileShape.FileGrid) {
-                return (
-                    <span className="mx_MFileBody">
-                        {placeholder}
-                        <div className="mx_MFileBody_download">
-                            <a className="mx_MFileBody_downloadLink" {...downloadProps}>
-                                { fileName }
-                            </a>
-                            <div className="mx_MImageBody_size">
-                                { content.info && content.info.size ? filesize(content.info.size) : "" }
-                            </div>
-                        </div>
-                    </span>
-                );
-            } else {
-                return (
-                    <span className="mx_MFileBody">
-                        {placeholder}
-                        <div className="mx_MFileBody_download">
-                            <a {...downloadProps}>
-                                <span className="mx_MFileBody_download_icon" />
-                                { _t("Download %(text)s", { text: text }) }
-                            </a>
-                        </div>
-                    </span>
-                );
-            }
+            return (
+                <span className="mx_MFileBody">
+                    { placeholder }
+                    { showDownloadLink && <div className="mx_MFileBody_download">
+                        <a {...downloadProps}>
+                            <span className="mx_MFileBody_download_icon" />
+                            { _t("Download %(text)s", { text: text }) }
+                        </a>
+                        { this.props.tileShape === TileShape.FileGrid && <div className="mx_MImageBody_size">
+                            { content.info && content.info.size ? filesize(content.info.size) : "" }
+                        </div> }
+                    </div> }
+                </span>
+            );
         } else {
             const extra = text ? (': ' + text) : '';
             return <span className="mx_MFileBody">
-                {placeholder}
+                { placeholder }
                 { _t("Invalid file%(extra)s", { extra: extra }) }
             </span>;
         }
diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx
index 96c8652aee..81b6cd634a 100644
--- a/src/components/views/messages/MImageBody.tsx
+++ b/src/components/views/messages/MImageBody.tsx
@@ -1,6 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2018 New Vector Ltd
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
 Copyright 2018, 2019 Michael Telatynski <7t3chguy@gmail.com>
 
 Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,7 +20,6 @@ import { Blurhash } from "react-blurhash";
 
 import MFileBody from './MFileBody';
 import Modal from '../../../Modal';
-import { decryptFile } from '../../../utils/DecryptFile';
 import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
@@ -29,24 +27,10 @@ import InlineSpinner from '../elements/InlineSpinner';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import { mediaFromContent } from "../../../customisations/Media";
 import { BLURHASH_FIELD } from "../../../ContentMessages";
-import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
-import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
 import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent';
 import ImageView from '../elements/ImageView';
 import { SyncState } from 'matrix-js-sdk/src/sync.api';
-
-export interface IProps {
-    /* the MatrixEvent to show */
-    mxEvent: MatrixEvent;
-    /* called when the image has loaded */
-    onHeightChanged(): void;
-
-    /* the maximum image height to use */
-    maxImageHeight?: number;
-
-    /* the permalinkCreator */
-    permalinkCreator?: RoomPermalinkCreator;
-}
+import { IBodyProps } from "./IBodyProps";
 
 interface IState {
     decryptedUrl?: string;
@@ -64,12 +48,12 @@ interface IState {
 }
 
 @replaceableComponent("views.messages.MImageBody")
-export default class MImageBody extends React.Component<IProps, IState> {
+export default class MImageBody extends React.Component<IBodyProps, IState> {
     static contextType = MatrixClientContext;
     private unmounted = true;
     private image = createRef<HTMLImageElement>();
 
-    constructor(props: IProps) {
+    constructor(props: IBodyProps) {
         super(props);
 
         this.state = {
@@ -257,38 +241,23 @@ export default class MImageBody extends React.Component<IProps, IState> {
         }
     }
 
-    private downloadImage(): void {
-        const content = this.props.mxEvent.getContent();
-        if (content.file !== undefined && this.state.decryptedUrl === null) {
-            let thumbnailPromise = Promise.resolve(null);
-            if (content.info && content.info.thumbnail_file) {
-                thumbnailPromise = decryptFile(
-                    content.info.thumbnail_file,
-                ).then(function(blob) {
-                    return URL.createObjectURL(blob);
+    private async downloadImage() {
+        if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) {
+            try {
+                const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value;
+                this.setState({
+                    decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
+                    decryptedThumbnailUrl: thumbnailUrl,
+                    decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
                 });
-            }
-            let decryptedBlob;
-            thumbnailPromise.then((thumbnailUrl) => {
-                return decryptFile(content.file).then(function(blob) {
-                    decryptedBlob = blob;
-                    return URL.createObjectURL(blob);
-                }).then((contentUrl) => {
-                    if (this.unmounted) return;
-                    this.setState({
-                        decryptedUrl: contentUrl,
-                        decryptedThumbnailUrl: thumbnailUrl,
-                        decryptedBlob: decryptedBlob,
-                    });
-                });
-            }).catch((err) => {
+            } catch (err) {
                 if (this.unmounted) return;
                 console.warn("Unable to decrypt attachment: ", err);
                 // Set a placeholder image when we can't decrypt the image.
                 this.setState({
                     error: err,
                 });
-            });
+            }
         }
     }
 
@@ -300,29 +269,15 @@ export default class MImageBody extends React.Component<IProps, IState> {
             localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true";
 
         if (showImage) {
-            // Don't download anything becaue we don't want to display anything.
+            // noinspection JSIgnoredPromiseFromCall
             this.downloadImage();
             this.setState({ showImage: true });
-        }
-
-        this._afterComponentDidMount();
-    }
-
-    // To be overridden by subclasses (e.g. MStickerBody) for further
-    // initialisation after componentDidMount
-    _afterComponentDidMount() {
+        } // else don't download anything because we don't want to display anything.
     }
 
     componentWillUnmount() {
         this.unmounted = true;
         this.context.removeListener('sync', this.onClientSync);
-
-        if (this.state.decryptedUrl) {
-            URL.revokeObjectURL(this.state.decryptedUrl);
-        }
-        if (this.state.decryptedThumbnailUrl) {
-            URL.revokeObjectURL(this.state.decryptedThumbnailUrl);
-        }
     }
 
     protected messageContent(
@@ -432,7 +387,7 @@ export default class MImageBody extends React.Component<IProps, IState> {
     // Overidden by MStickerBody
     protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
         return <a href={contentUrl} onClick={this.onClick}>
-            {children}
+            { children }
         </a>;
     }
 
@@ -440,9 +395,9 @@ export default class MImageBody extends React.Component<IProps, IState> {
     protected getPlaceholder(width: number, height: number): JSX.Element {
         const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD];
         if (blurhash) return <Blurhash hash={blurhash} width={width} height={height} />;
-        return <div className="mx_MImageBody_thumbnail_spinner">
+        return (
             <InlineSpinner w={32} h={32} />
-        </div>;
+        );
     }
 
     // Overidden by MStickerBody
@@ -452,7 +407,10 @@ export default class MImageBody extends React.Component<IProps, IState> {
 
     // Overidden by MStickerBody
     protected getFileBody(): JSX.Element {
-        return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
+        // We only ever need the download bar if we're appearing outside of the timeline
+        if (this.props.tileShape) {
+            return <MFileBody {...this.props} showGenericPlaceholder={false} />;
+        }
     }
 
     render() {
@@ -499,7 +457,7 @@ export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProp
             <div className={className} style={{ maxWidth: maxWidth }}>
                 <div className='mx_HiddenImagePlaceholder_button'>
                     <span className='mx_HiddenImagePlaceholder_eye' />
-                    <span>{_t("Show image")}</span>
+                    <span>{ _t("Show image") }</span>
                 </div>
             </div>
         );
diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx
index 44acf18004..5f7f0da3ca 100644
--- a/src/components/views/messages/MImageReplyBody.tsx
+++ b/src/components/views/messages/MImageReplyBody.tsx
@@ -33,7 +33,7 @@ export default class MImageReplyBody extends MImageBody {
 
     // Don't show "Download this_file.png ..."
     public getFileBody(): JSX.Element {
-        return presentableTextForFile(this.props.mxEvent.getContent());
+        return <>{ presentableTextForFile(this.props.mxEvent.getContent()) }</>;
     }
 
     render() {
diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx
index c57cb5932d..ce828beed0 100644
--- a/src/components/views/messages/MKeyVerificationRequest.tsx
+++ b/src/components/views/messages/MKeyVerificationRequest.tsx
@@ -131,7 +131,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
             const accepted = request.ready || request.started || request.done;
             if (accepted) {
                 stateLabel = (<AccessibleButton onClick={this.openRequest}>
-                    {this.acceptedLabel(request.receivingUserId)}
+                    { this.acceptedLabel(request.receivingUserId) }
                 </AccessibleButton>);
             } else if (request.cancelled) {
                 stateLabel = this.cancelledLabel(request.cancellingUserId);
@@ -140,7 +140,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
             } else if (request.declining) {
                 stateLabel = _t("Declining …");
             }
-            stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
+            stateNode = (<div className="mx_cryptoEvent_state">{ stateLabel }</div>);
         }
 
         if (!request.initiatedByMe) {
@@ -150,10 +150,10 @@ export default class MKeyVerificationRequest extends React.Component<IProps> {
             if (request.canAccept) {
                 stateNode = (<div className="mx_cryptoEvent_buttons">
                     <AccessibleButton kind="danger" onClick={this.onRejectClicked}>
-                        {_t("Decline")}
+                        { _t("Decline") }
                     </AccessibleButton>
                     <AccessibleButton kind="primary" onClick={this.onAcceptClicked}>
-                        {_t("Accept")}
+                        { _t("Accept") }
                     </AccessibleButton>
                 </div>);
             }
diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx
index d882bb1eb0..6121a23752 100644
--- a/src/components/views/messages/MVideoBody.tsx
+++ b/src/components/views/messages/MVideoBody.tsx
@@ -1,6 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -18,21 +17,15 @@ limitations under the License.
 import React from 'react';
 import { decode } from "blurhash";
 
-import MFileBody from './MFileBody';
-import { decryptFile } from '../../../utils/DecryptFile';
 import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import InlineSpinner from '../elements/InlineSpinner';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import { mediaFromContent } from "../../../customisations/Media";
 import { BLURHASH_FIELD } from "../../../ContentMessages";
-
-interface IProps {
-    /* the MatrixEvent to show */
-    mxEvent: any;
-    /* called when the video has loaded */
-    onHeightChanged: () => void;
-}
+import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
+import { IBodyProps } from "./IBodyProps";
+import MFileBody from "./MFileBody";
 
 interface IState {
     decryptedUrl?: string;
@@ -45,11 +38,12 @@ interface IState {
 }
 
 @replaceableComponent("views.messages.MVideoBody")
-export default class MVideoBody extends React.PureComponent<IProps, IState> {
+export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
     private videoRef = React.createRef<HTMLVideoElement>();
 
     constructor(props) {
         super(props);
+
         this.state = {
             fetchingData: false,
             decryptedUrl: null,
@@ -97,7 +91,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
     }
 
     private getThumbUrl(): string|null {
-        const content = this.props.mxEvent.getContent();
+        const content = this.props.mxEvent.getContent<IMediaEventContent>();
         const media = mediaFromContent(content);
 
         if (media.isEncrypted && this.state.decryptedThumbnailUrl) {
@@ -139,7 +133,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
             posterLoading: true,
         });
 
-        const content = this.props.mxEvent.getContent();
+        const content = this.props.mxEvent.getContent<IMediaEventContent>();
         const media = mediaFromContent(content);
         if (media.hasThumbnail) {
             const image = new Image();
@@ -152,30 +146,22 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
 
     async componentDidMount() {
         const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean;
-        const content = this.props.mxEvent.getContent();
         this.loadBlurhash();
 
-        if (content.file !== undefined && this.state.decryptedUrl === null) {
-            let thumbnailPromise = Promise.resolve(null);
-            if (content?.info?.thumbnail_file) {
-                thumbnailPromise = decryptFile(content.info.thumbnail_file)
-                    .then(blob => URL.createObjectURL(blob));
-            }
-
+        if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) {
             try {
-                const thumbnailUrl = await thumbnailPromise;
+                const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value;
                 if (autoplay) {
                     console.log("Preloading video");
-                    const decryptedBlob = await decryptFile(content.file);
-                    const contentUrl = URL.createObjectURL(decryptedBlob);
                     this.setState({
-                        decryptedUrl: contentUrl,
+                        decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
                         decryptedThumbnailUrl: thumbnailUrl,
-                        decryptedBlob: decryptedBlob,
+                        decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
                     });
                     this.props.onHeightChanged();
                 } else {
                     console.log("NOT preloading video");
+                    const content = this.props.mxEvent.getContent<IMediaEventContent>();
                     this.setState({
                         // For Chrome and Electron, we need to set some non-empty `src` to
                         // enable the play button. Firefox does not seem to care either
@@ -195,15 +181,6 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
         }
     }
 
-    componentWillUnmount() {
-        if (this.state.decryptedUrl) {
-            URL.revokeObjectURL(this.state.decryptedUrl);
-        }
-        if (this.state.decryptedThumbnailUrl) {
-            URL.revokeObjectURL(this.state.decryptedThumbnailUrl);
-        }
-    }
-
     private videoOnPlay = async () => {
         if (this.hasContentUrl() || this.state.fetchingData || this.state.error) {
             // We have the file, we are fetching the file, or there is an error.
@@ -213,18 +190,15 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
             // To stop subsequent download attempts
             fetchingData: true,
         });
-        const content = this.props.mxEvent.getContent();
-        if (!content.file) {
+        if (!this.props.mediaEventHelper.media.isEncrypted) {
             this.setState({
                 error: "No file given in content",
             });
             return;
         }
-        const decryptedBlob = await decryptFile(content.file);
-        const contentUrl = URL.createObjectURL(decryptedBlob);
         this.setState({
-            decryptedUrl: contentUrl,
-            decryptedBlob: decryptedBlob,
+            decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value,
+            decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value,
             fetchingData: false,
         }, () => {
             if (!this.videoRef.current) return;
@@ -295,7 +269,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
                     onPlay={this.videoOnPlay}
                 >
                 </video>
-                <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
+                { this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
             </span>
         );
     }
diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx
index bec224dd2d..f184caf448 100644
--- a/src/components/views/messages/MVoiceMessageBody.tsx
+++ b/src/components/views/messages/MVoiceMessageBody.tsx
@@ -15,73 +15,16 @@ limitations under the License.
 */
 
 import React from "react";
-import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
-import { Playback } from "../../../voice/Playback";
-import MFileBody from "./MFileBody";
 import InlineSpinner from '../elements/InlineSpinner';
 import { _t } from "../../../languageHandler";
-import { mediaFromContent } from "../../../customisations/Media";
-import { decryptFile } from "../../../utils/DecryptFile";
 import RecordingPlayback from "../audio_messages/RecordingPlayback";
-import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
-import { TileShape } from "../rooms/EventTile";
-
-interface IProps {
-    mxEvent: MatrixEvent;
-    tileShape?: TileShape;
-}
-
-interface IState {
-    error?: Error;
-    playback?: Playback;
-    decryptedBlob?: Blob;
-}
+import MAudioBody from "./MAudioBody";
+import MFileBody from "./MFileBody";
 
 @replaceableComponent("views.messages.MVoiceMessageBody")
-export default class MVoiceMessageBody extends React.PureComponent<IProps, IState> {
-    constructor(props: IProps) {
-        super(props);
-
-        this.state = {};
-    }
-
-    public async componentDidMount() {
-        let buffer: ArrayBuffer;
-        const content: IMediaEventContent = this.props.mxEvent.getContent();
-        const media = mediaFromContent(content);
-        if (media.isEncrypted) {
-            try {
-                const blob = await decryptFile(content.file);
-                buffer = await blob.arrayBuffer();
-                this.setState({ decryptedBlob: blob });
-            } catch (e) {
-                this.setState({ error: e });
-                console.warn("Unable to decrypt voice message", e);
-                return; // stop processing the audio file
-            }
-        } else {
-            try {
-                buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer());
-            } catch (e) {
-                this.setState({ error: e });
-                console.warn("Unable to download voice message", e);
-                return; // stop processing the audio file
-            }
-        }
-
-        const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024);
-
-        // We should have a buffer to work with now: let's set it up
-        const playback = new Playback(buffer, waveform);
-        this.setState({ playback });
-        // Note: the RecordingPlayback component will handle preparing the Playback class for us.
-    }
-
-    public componentWillUnmount() {
-        this.state.playback?.destroy();
-    }
-
+export default class MVoiceMessageBody extends MAudioBody {
+    // A voice message is an audio file but rendered in a special way.
     public render() {
         if (this.state.error) {
             // TODO: @@TR: Verify error state
@@ -106,7 +49,7 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
         return (
             <span className="mx_MVoiceMessageBody">
                 <RecordingPlayback playback={this.state.playback} tileShape={this.props.tileShape} />
-                <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
+                { this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
             </span>
         );
     }
diff --git a/src/components/views/messages/MVoiceOrAudioBody.tsx b/src/components/views/messages/MVoiceOrAudioBody.tsx
index 676b5a2c47..adfd102e19 100644
--- a/src/components/views/messages/MVoiceOrAudioBody.tsx
+++ b/src/components/views/messages/MVoiceOrAudioBody.tsx
@@ -15,18 +15,14 @@ limitations under the License.
 */
 
 import React from "react";
-import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import MAudioBody from "./MAudioBody";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import SettingsStore from "../../../settings/SettingsStore";
 import MVoiceMessageBody from "./MVoiceMessageBody";
-
-interface IProps {
-    mxEvent: MatrixEvent;
-}
+import { IBodyProps } from "./IBodyProps";
 
 @replaceableComponent("views.messages.MVoiceOrAudioBody")
-export default class MVoiceOrAudioBody extends React.PureComponent<IProps> {
+export default class MVoiceOrAudioBody extends React.PureComponent<IBodyProps> {
     public render() {
         // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
         const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js
index 7532554666..7fe0eca697 100644
--- a/src/components/views/messages/MessageActionBar.js
+++ b/src/components/views/messages/MessageActionBar.js
@@ -32,6 +32,8 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
 import { canCancel } from "../context_menus/MessageContextMenu";
 import Resend from "../../../Resend";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { MediaEventHelper } from "../../../utils/MediaEventHelper";
+import DownloadActionButton from "./DownloadActionButton";
 
 const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => {
     const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@@ -267,6 +269,15 @@ export default class MessageActionBar extends React.PureComponent {
                         key="react"
                     />);
                 }
+
+                // XXX: Assuming that the underlying tile will be a media event if it is eligible media.
+                if (MediaEventHelper.isEligible(this.props.mxEvent)) {
+                    toolbarOpts.splice(0, 0, <DownloadActionButton
+                        mxEvent={this.props.mxEvent}
+                        mediaEventHelperGet={() => this.props.getTile?.().getMediaHelper?.()}
+                        key="download"
+                    />);
+                }
             }
 
             if (allowCancel) {
@@ -286,7 +297,7 @@ export default class MessageActionBar extends React.PureComponent {
 
         // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
         return <Toolbar className="mx_MessageActionBar" aria-label={_t("Message Actions")} aria-live="off">
-            {toolbarOpts}
+            { toolbarOpts }
         </Toolbar>;
     }
 }
diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.tsx
similarity index 50%
rename from src/components/views/messages/MessageEvent.js
rename to src/components/views/messages/MessageEvent.tsx
index cd071ebb34..53592e3985 100644
--- a/src/components/views/messages/MessageEvent.js
+++ b/src/components/views/messages/MessageEvent.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,90 +15,98 @@ limitations under the License.
 */
 
 import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
 import SettingsStore from "../../../settings/SettingsStore";
 import { Mjolnir } from "../../../mjolnir/Mjolnir";
 import RedactedBody from "./RedactedBody";
 import UnknownBody from "./UnknownBody";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { IMediaBody } from "./IMediaBody";
+import { IOperableEventTile } from "../context_menus/MessageContextMenu";
+import { MediaEventHelper } from "../../../utils/MediaEventHelper";
+import { ReactAnyComponent } from "../../../@types/common";
+import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
+import { IBodyProps } from "./IBodyProps";
+
+// onMessageAllowed is handled internally
+interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
+    /* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
+    overrideBodyTypes?: Record<string, React.Component>;
+    overrideEventTypes?: Record<string, React.Component>;
+}
 
 @replaceableComponent("views.messages.MessageEvent")
-export default class MessageEvent extends React.Component {
-    static propTypes = {
-        /* the MatrixEvent to show */
-        mxEvent: PropTypes.object.isRequired,
+export default class MessageEvent extends React.Component<IProps> implements IMediaBody, IOperableEventTile {
+    private body: React.RefObject<React.Component | IOperableEventTile> = createRef();
+    private mediaHelper: MediaEventHelper;
 
-        /* a list of words to highlight */
-        highlights: PropTypes.array,
-
-        /* link URL for the highlights */
-        highlightLink: PropTypes.string,
-
-        /* should show URL previews for this event */
-        showUrlPreview: PropTypes.bool,
-
-        /* callback called when dynamic content in events are loaded */
-        onHeightChanged: PropTypes.func,
-
-        /* the shape of the tile, used */
-        tileShape: PropTypes.string, // TODO: Use TileShape enum
-
-        /* the maximum image height to use, if the event is an image */
-        maxImageHeight: PropTypes.number,
-
-        /* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
-        overrideBodyTypes: PropTypes.object,
-        overrideEventTypes: PropTypes.object,
-
-        /* the permalinkCreator */
-        permalinkCreator: PropTypes.object,
-    };
-
-    constructor(props) {
+    public constructor(props: IProps) {
         super(props);
 
-        this._body = createRef();
+        if (MediaEventHelper.isEligible(this.props.mxEvent)) {
+            this.mediaHelper = new MediaEventHelper(this.props.mxEvent);
+        }
     }
 
-    getEventTileOps = () => {
-        return this._body.current && this._body.current.getEventTileOps ? this._body.current.getEventTileOps() : null;
-    };
+    public componentWillUnmount() {
+        this.mediaHelper?.destroy();
+    }
 
-    onTileUpdate = () => {
-        this.forceUpdate();
-    };
+    public componentDidUpdate(prevProps: Readonly<IProps>) {
+        if (this.props.mxEvent !== prevProps.mxEvent && MediaEventHelper.isEligible(this.props.mxEvent)) {
+            this.mediaHelper?.destroy();
+            this.mediaHelper = new MediaEventHelper(this.props.mxEvent);
+        }
+    }
 
-    render() {
-        const bodyTypes = {
-            'm.text': sdk.getComponent('messages.TextualBody'),
-            'm.notice': sdk.getComponent('messages.TextualBody'),
-            'm.emote': sdk.getComponent('messages.TextualBody'),
-            'm.image': sdk.getComponent('messages.MImageBody'),
-            'm.file': sdk.getComponent('messages.MFileBody'),
-            'm.audio': sdk.getComponent('messages.MVoiceOrAudioBody'),
-            'm.video': sdk.getComponent('messages.MVideoBody'),
+    private get bodyTypes(): Record<string, React.Component> {
+        return {
+            [MsgType.Text]: sdk.getComponent('messages.TextualBody'),
+            [MsgType.Notice]: sdk.getComponent('messages.TextualBody'),
+            [MsgType.Emote]: sdk.getComponent('messages.TextualBody'),
+            [MsgType.Image]: sdk.getComponent('messages.MImageBody'),
+            [MsgType.File]: sdk.getComponent('messages.MFileBody'),
+            [MsgType.Audio]: sdk.getComponent('messages.MVoiceOrAudioBody'),
+            [MsgType.Video]: sdk.getComponent('messages.MVideoBody'),
 
             ...(this.props.overrideBodyTypes || {}),
         };
-        const evTypes = {
-            'm.sticker': sdk.getComponent('messages.MStickerBody'),
+    }
+
+    private get evTypes(): Record<string, React.Component> {
+        return {
+            [EventType.Sticker]: sdk.getComponent('messages.MStickerBody'),
+
             ...(this.props.overrideEventTypes || {}),
         };
+    }
 
+    public getEventTileOps = () => {
+        return (this.body.current as IOperableEventTile)?.getEventTileOps?.() || null;
+    };
+
+    public getMediaHelper() {
+        return this.mediaHelper;
+    }
+
+    private onTileUpdate = () => {
+        this.forceUpdate();
+    };
+
+    public render() {
         const content = this.props.mxEvent.getContent();
         const type = this.props.mxEvent.getType();
         const msgtype = content.msgtype;
-        let BodyType = RedactedBody;
+        let BodyType: ReactAnyComponent = RedactedBody;
         if (!this.props.mxEvent.isRedacted()) {
             // only resolve BodyType if event is not redacted
-            if (type && evTypes[type]) {
-                BodyType = evTypes[type];
-            } else if (msgtype && bodyTypes[msgtype]) {
-                BodyType = bodyTypes[msgtype];
+            if (type && this.evTypes[type]) {
+                BodyType = this.evTypes[type];
+            } else if (msgtype && this.bodyTypes[msgtype]) {
+                BodyType = this.bodyTypes[msgtype];
             } else if (content.url) {
                 // Fallback to MFileBody if there's a content URL
-                BodyType = bodyTypes['m.file'];
+                BodyType = this.bodyTypes[MsgType.File];
             } else {
                 // Fallback to UnknownBody otherwise if not redacted
                 BodyType = UnknownBody;
@@ -120,8 +128,9 @@ export default class MessageEvent extends React.Component {
             }
         }
 
+        // @ts-ignore - this is a dynamic react component
         return BodyType ? <BodyType
-            ref={this._body}
+            ref={this.body}
             mxEvent={this.props.mxEvent}
             highlights={this.props.highlights}
             highlightLink={this.props.highlightLink}
@@ -133,6 +142,7 @@ export default class MessageEvent extends React.Component {
             onHeightChanged={this.props.onHeightChanged}
             onMessageAllowed={this.onTileUpdate}
             permalinkCreator={this.props.permalinkCreator}
+            mediaEventHelper={this.mediaHelper}
         /> : null;
     }
 }
diff --git a/src/components/views/messages/MessageTimestamp.tsx b/src/components/views/messages/MessageTimestamp.tsx
index 8b02f6b38e..a657032c86 100644
--- a/src/components/views/messages/MessageTimestamp.tsx
+++ b/src/components/views/messages/MessageTimestamp.tsx
@@ -45,7 +45,7 @@ export default class MessageTimestamp extends React.Component<IProps> {
                 title={formatFullDate(date, this.props.showTwelveHour)}
                 aria-hidden={true}
             >
-                {timestamp}
+                { timestamp }
             </span>
         );
     }
diff --git a/src/components/views/messages/MjolnirBody.js b/src/components/views/messages/MjolnirBody.js
index 67484a6d9c..23f255b569 100644
--- a/src/components/views/messages/MjolnirBody.js
+++ b/src/components/views/messages/MjolnirBody.js
@@ -41,10 +41,10 @@ export default class MjolnirBody extends React.Component {
 
     render() {
         return (
-            <div className='mx_MjolnirBody'><i>{_t(
+            <div className='mx_MjolnirBody'><i>{ _t(
                 "You have ignored this user, so their message is hidden. <a>Show anyways.</a>",
-                {}, { a: (sub) => <a href="#" onClick={this._onAllowClick}>{sub}</a> },
-            )}</i></div>
+                {}, { a: (sub) => <a href="#" onClick={this._onAllowClick}>{ sub }</a> },
+            ) }</i></div>
         );
     }
 }
diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx
index 55ffb8deac..d4caf4ecf8 100644
--- a/src/components/views/messages/ReactionsRow.tsx
+++ b/src/components/views/messages/ReactionsRow.tsx
@@ -199,7 +199,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
                 href="#"
                 onClick={this.onShowAllClick}
             >
-                {_t("Show all")}
+                { _t("Show all") }
             </a>;
         }
 
diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx
index 53e27b882e..7498a49173 100644
--- a/src/components/views/messages/ReactionsRowButton.tsx
+++ b/src/components/views/messages/ReactionsRowButton.tsx
@@ -142,12 +142,12 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
             onMouseLeave={this.onMouseLeave}
         >
             <span className="mx_ReactionsRowButton_content" aria-hidden="true">
-                {content}
+                { content }
             </span>
             <span className="mx_ReactionsRowButton_count" aria-hidden="true">
-                {count}
+                { count }
             </span>
-            {tooltip}
+            { tooltip }
         </AccessibleButton>;
     }
 }
diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.tsx b/src/components/views/messages/ReactionsRowButtonTooltip.tsx
index e60174530a..9c43c0df77 100644
--- a/src/components/views/messages/ReactionsRowButtonTooltip.tsx
+++ b/src/components/views/messages/ReactionsRowButtonTooltip.tsx
@@ -51,7 +51,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
                 senders.push(name);
             }
             const shortName = unicodeToShortcode(content);
-            tooltipLabel = <div>{_t(
+            tooltipLabel = <div>{ _t(
                 "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
                 {
                     shortName,
@@ -59,7 +59,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
                 {
                     reactors: () => {
                         return <div className="mx_Tooltip_title">
-                            {formatCommaSeparatedList(senders, 6)}
+                            { formatCommaSeparatedList(senders, 6) }
                         </div>;
                     },
                     reactedWith: (sub) => {
@@ -67,11 +67,11 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent<IProp
                             return null;
                         }
                         return <div className="mx_Tooltip_sub">
-                            {sub}
+                            { sub }
                         </div>;
                     },
                 },
-            )}</div>;
+            ) }</div>;
         }
 
         let tooltip;
diff --git a/src/components/views/messages/RedactedBody.tsx b/src/components/views/messages/RedactedBody.tsx
index 3e5da1dd43..c2e137c97b 100644
--- a/src/components/views/messages/RedactedBody.tsx
+++ b/src/components/views/messages/RedactedBody.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,17 +16,13 @@ limitations under the License.
 
 import React, { useContext } from "react";
 import { MatrixClient } from "matrix-js-sdk/src/client";
-import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { _t } from "../../../languageHandler";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import { formatFullDate } from "../../../DateUtils";
 import SettingsStore from "../../../settings/SettingsStore";
+import { IBodyProps } from "./IBodyProps";
 
-interface IProps {
-    mxEvent: MatrixEvent;
-}
-
-const RedactedBody = React.forwardRef<any, IProps>(({ mxEvent }, ref) => {
+const RedactedBody = React.forwardRef<any, IBodyProps>(({ mxEvent }, ref) => {
     const cli: MatrixClient = useContext(MatrixClientContext);
 
     let text = _t("Message deleted");
diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js
index 56bd25cbac..a0bc8daa64 100644
--- a/src/components/views/messages/RoomCreate.js
+++ b/src/components/views/messages/RoomCreate.js
@@ -56,7 +56,7 @@ export default class RoomCreate extends React.Component {
         const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']);
         const link = (
             <a href={predecessorPermalink} onClick={this._onLinkClicked}>
-                {_t("Click here to see older messages.")}
+                { _t("Click here to see older messages.") }
             </a>
         );
 
diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx
index 9009b9ee1b..666fc1cbe0 100644
--- a/src/components/views/messages/TextualBody.tsx
+++ b/src/components/views/messages/TextualBody.tsx
@@ -17,7 +17,6 @@ limitations under the License.
 import React, { createRef, SyntheticEvent } from 'react';
 import ReactDOM from 'react-dom';
 import highlight from 'highlight.js';
-import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
 import { MsgType } from "matrix-js-sdk/src/@types/event";
 
 import * as HtmlUtils from '../../../HtmlUtils';
@@ -38,37 +37,13 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
 import UIStore from "../../../stores/UIStore";
 import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
 import { Action } from "../../../dispatcher/actions";
-import { TileShape } from '../rooms/EventTile';
-import EditorStateTransfer from "../../../utils/EditorStateTransfer";
 import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
 import Spoiler from "../elements/Spoiler";
 import QuestionDialog from "../dialogs/QuestionDialog";
 import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
 import EditMessageComposer from '../rooms/EditMessageComposer';
 import LinkPreviewGroup from '../rooms/LinkPreviewGroup';
-
-interface IProps {
-    /* the MatrixEvent to show */
-    mxEvent: MatrixEvent;
-
-    /* a list of words to highlight */
-    highlights?: string[];
-
-    /* link URL for the highlights */
-    highlightLink?: string;
-
-    /* should show URL previews for this event */
-    showUrlPreview?: boolean;
-
-    /* the shape of the tile, used */
-    tileShape?: TileShape;
-
-    editState?: EditorStateTransfer;
-    replacingEventId?: string;
-
-    /* callback for when our widget has loaded */
-    onHeightChanged(): void;
-}
+import { IBodyProps } from "./IBodyProps";
 
 interface IState {
     // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody.
@@ -79,7 +54,7 @@ interface IState {
 }
 
 @replaceableComponent("views.messages.TextualBody")
-export default class TextualBody extends React.Component<IProps, IState> {
+export default class TextualBody extends React.Component<IBodyProps, IState> {
     private readonly contentRef = createRef<HTMLSpanElement>();
 
     private unmounted = false;
@@ -475,10 +450,10 @@ export default class TextualBody extends React.Component<IProps, IState> {
 
         const tooltip = <div>
             <div className="mx_Tooltip_title">
-                {_t("Edited at %(date)s", { date: dateString })}
+                { _t("Edited at %(date)s", { date: dateString }) }
             </div>
             <div className="mx_Tooltip_sub">
-                {_t("Click to view edits")}
+                { _t("Click to view edits") }
             </div>
         </div>;
 
@@ -489,7 +464,7 @@ export default class TextualBody extends React.Component<IProps, IState> {
                 title={_t("Edited at %(date)s. Click to view edits.", { date: dateString })}
                 tooltip={tooltip}
             >
-                <span>{`(${_t("edited")})`}</span>
+                <span>{ `(${_t("edited")})` }</span>
             </AccessibleTooltipButton>
         );
     }
@@ -513,8 +488,8 @@ export default class TextualBody extends React.Component<IProps, IState> {
         });
         if (this.props.replacingEventId) {
             body = <>
-                {body}
-                {this.renderEditedMarker()}
+                { body }
+                { this.renderEditedMarker() }
             </>;
         }
 
diff --git a/src/components/views/messages/TileErrorBoundary.tsx b/src/components/views/messages/TileErrorBoundary.tsx
index 967127d275..c61771f396 100644
--- a/src/components/views/messages/TileErrorBoundary.tsx
+++ b/src/components/views/messages/TileErrorBoundary.tsx
@@ -67,14 +67,14 @@ export default class TileErrorBoundary extends React.Component<IProps, IState> {
             let submitLogsButton;
             if (SdkConfig.get().bug_report_endpoint_url) {
                 submitLogsButton = <a onClick={this.onBugReport} href="#">
-                    {_t("Submit logs")}
+                    { _t("Submit logs") }
                 </a>;
             }
 
             return (<div className={classNames(classes)}>
                 <div className="mx_EventTile_line">
                     <span>
-                        {_t("Can't load this message")}
+                        { _t("Can't load this message") }
                         { mxEvent && ` (${mxEvent.getType()})` }
                         { submitLogsButton }
                     </span>
diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.tsx
similarity index 76%
rename from src/components/views/messages/UnknownBody.js
rename to src/components/views/messages/UnknownBody.tsx
index 0f866216fc..b09afa54e9 100644
--- a/src/components/views/messages/UnknownBody.js
+++ b/src/components/views/messages/UnknownBody.tsx
@@ -16,12 +16,19 @@ limitations under the License.
 */
 
 import React, { forwardRef } from "react";
+import { MatrixEvent } from "matrix-js-sdk/src";
 
-export default forwardRef(({ mxEvent }, ref) => {
+interface IProps {
+    mxEvent: MatrixEvent;
+    children?: React.ReactNode;
+}
+
+export default forwardRef(({ mxEvent, children }: IProps, ref: React.RefObject<HTMLSpanElement>) => {
     const text = mxEvent.getContent().body;
     return (
         <span className="mx_UnknownBody" ref={ref}>
             { text }
+            { children }
         </span>
     );
 });
diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js
index 62454fef1a..5a5c015dcf 100644
--- a/src/components/views/messages/ViewSourceEvent.js
+++ b/src/components/views/messages/ViewSourceEvent.js
@@ -60,9 +60,9 @@ export default class ViewSourceEvent extends React.PureComponent {
 
         let content;
         if (expanded) {
-            content = <pre>{JSON.stringify(mxEvent, null, 4)}</pre>;
+            content = <pre>{ JSON.stringify(mxEvent, null, 4) }</pre>;
         } else {
-            content = <code>{`{ "type": ${mxEvent.getType()} }`}</code>;
+            content = <code>{ `{ "type": ${mxEvent.getType()} }` }</code>;
         }
 
         const classes = classNames("mx_ViewSourceEvent mx_EventTile_content", {
@@ -70,7 +70,7 @@ export default class ViewSourceEvent extends React.PureComponent {
         });
 
         return <span className={classes}>
-            {content}
+            { content }
             <a
                 className="mx_ViewSourceEvent_toggle"
                 href="#"
diff --git a/src/components/views/right_panel/BaseCard.tsx b/src/components/views/right_panel/BaseCard.tsx
index 2528139a2b..54bf6e769c 100644
--- a/src/components/views/right_panel/BaseCard.tsx
+++ b/src/components/views/right_panel/BaseCard.tsx
@@ -43,8 +43,8 @@ interface IGroupProps {
 
 export const Group: React.FC<IGroupProps> = ({ className, title, children }) => {
     return <div className={classNames("mx_BaseCard_Group", className)}>
-        <h1>{title}</h1>
-        {children}
+        <h1>{ title }</h1>
+        { children }
     </div>;
 };
 
diff --git a/src/components/views/right_panel/EncryptionInfo.tsx b/src/components/views/right_panel/EncryptionInfo.tsx
index e74caf8457..34aeb8b88a 100644
--- a/src/components/views/right_panel/EncryptionInfo.tsx
+++ b/src/components/views/right_panel/EncryptionInfo.tsx
@@ -66,7 +66,7 @@ const EncryptionInfo: React.FC<IProps> = ({
     } else {
         content = (
             <AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={onStartVerification}>
-                {_t("Start Verification")}
+                { _t("Start Verification") }
             </AccessibleButton>
         );
     }
@@ -75,17 +75,17 @@ const EncryptionInfo: React.FC<IProps> = ({
     if (isRoomEncrypted) {
         description = (
             <div>
-                <p>{_t("Messages in this room are end-to-end encrypted.")}</p>
-                <p>{_t("Your messages are secured and only you and the recipient have " +
-                    "the unique keys to unlock them.")}</p>
+                <p>{ _t("Messages in this room are end-to-end encrypted.") }</p>
+                <p>{ _t("Your messages are secured and only you and the recipient have " +
+                    "the unique keys to unlock them.") }</p>
             </div>
         );
     } else {
         description = (
             <div>
-                <p>{_t("Messages in this room are not end-to-end encrypted.")}</p>
-                <p>{_t("In encrypted rooms, your messages are secured and only you and the recipient have " +
-                    "the unique keys to unlock them.")}</p>
+                <p>{ _t("Messages in this room are not end-to-end encrypted.") }</p>
+                <p>{ _t("In encrypted rooms, your messages are secured and only you and the recipient have " +
+                    "the unique keys to unlock them.") }</p>
             </div>
         );
     }
@@ -96,14 +96,14 @@ const EncryptionInfo: React.FC<IProps> = ({
 
     return <React.Fragment>
         <div className="mx_UserInfo_container">
-            <h3>{_t("Encryption")}</h3>
+            <h3>{ _t("Encryption") }</h3>
             { description }
         </div>
         <div className="mx_UserInfo_container">
-            <h3>{_t("Verify User")}</h3>
+            <h3>{ _t("Verify User") }</h3>
             <div>
-                <p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
-                <p>{_t("To be secure, do this in person or use a trusted way to communicate.")}</p>
+                <p>{ _t("For extra security, verify this user by checking a one-time code on both of your devices.") }</p>
+                <p>{ _t("To be secure, do this in person or use a trusted way to communicate.") }</p>
                 { content }
             </div>
         </div>
diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx
index 9ed791c229..b1c8d427bf 100644
--- a/src/components/views/right_panel/EncryptionPanel.tsx
+++ b/src/components/views/right_panel/EncryptionPanel.tsx
@@ -87,12 +87,12 @@ const EncryptionPanel: React.FC<IProps> = (props: IProps) => {
                 headerImage: require("../../../../res/img/e2e/warning.svg"),
                 title: _t("Your messages are not secure"),
                 description: <div>
-                    {_t("One of the following may be compromised:")}
+                    { _t("One of the following may be compromised:") }
                     <ul>
-                        <li>{_t("Your homeserver")}</li>
-                        <li>{_t("The homeserver the user you’re verifying is connected to")}</li>
-                        <li>{_t("Yours, or the other users’ internet connection")}</li>
-                        <li>{_t("Yours, or the other users’ session")}</li>
+                        <li>{ _t("Your homeserver") }</li>
+                        <li>{ _t("The homeserver the user you’re verifying is connected to") }</li>
+                        <li>{ _t("Yours, or the other users’ internet connection") }</li>
+                        <li>{ _t("Yours, or the other users’ session") }</li>
                     </ul>
                 </div>,
                 onFinished: onClose,
diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx
index c30ad60771..8d000a29fc 100644
--- a/src/components/views/right_panel/HeaderButtons.tsx
+++ b/src/components/views/right_panel/HeaderButtons.tsx
@@ -99,7 +99,7 @@ export default abstract class HeaderButtons<P = {}> extends React.Component<IPro
 
     public render() {
         return <div className="mx_HeaderButtons">
-            {this.renderButtons()}
+            { this.renderButtons() }
         </div>;
     }
 }
diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 21c1c39827..047448d925 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -148,7 +148,7 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
             yOffset={-48}
         >
             <WidgetAvatar app={app} />
-            <span>{name}</span>
+            <span>{ name }</span>
             { subtitle }
         </AccessibleTooltipButton>
 
@@ -256,7 +256,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
                 <h2 title={name}>
                     { name }
                 </h2>
-            )}
+            ) }
         </RoomName>
         <div className="mx_RoomSummaryCard_alias" title={alias}>
             { alias }
@@ -268,16 +268,16 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
     return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
         <Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
             <Button className="mx_RoomSummaryCard_icon_people" onClick={onRoomMembersClick}>
-                {_t("%(count)s people", { count: memberCount })}
+                { _t("%(count)s people", { count: memberCount }) }
             </Button>
             <Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
-                {_t("Show files")}
+                { _t("Show files") }
             </Button>
             <Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
-                {_t("Share room")}
+                { _t("Share room") }
             </Button>
             <Button className="mx_RoomSummaryCard_icon_settings" onClick={onRoomSettingsClick}>
-                {_t("Room settings")}
+                { _t("Room settings") }
             </Button>
         </Group>
 
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index fc3814136d..5e2b327a5f 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -207,8 +207,8 @@ function DeviceItem({ userId, device }: {userId: string, device: IDevice}) {
         return (
             <div className={classes} title={device.deviceId} >
                 <div className={iconClasses} />
-                <div className="mx_UserInfo_device_name">{deviceName}</div>
-                <div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
+                <div className="mx_UserInfo_device_name">{ deviceName }</div>
+                <div className="mx_UserInfo_device_trusted">{ trustedLabel }</div>
             </div>
         );
     } else {
@@ -219,8 +219,8 @@ function DeviceItem({ userId, device }: {userId: string, device: IDevice}) {
                 onClick={onDeviceClick}
             >
                 <div className={iconClasses} />
-                <div className="mx_UserInfo_device_name">{deviceName}</div>
-                <div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
+                <div className="mx_UserInfo_device_name">{ deviceName }</div>
+                <div className="mx_UserInfo_device_trusted">{ trustedLabel }</div>
             </AccessibleButton>
         );
     }
@@ -237,7 +237,7 @@ function DevicesSection({ devices, userId, loading }: {devices: IDevice[], userI
         return <Spinner />;
     }
     if (devices === null) {
-        return <>{_t("Unable to load session list")}</>;
+        return <>{ _t("Unable to load session list") }</>;
     }
     const isMe = userId === cli.getUserId();
     const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId));
@@ -282,14 +282,14 @@ function DevicesSection({ devices, userId, loading }: {devices: IDevice[], userI
             expandButton = (<AccessibleButton className="mx_UserInfo_expand mx_linkButton"
                 onClick={() => setExpanded(false)}
             >
-                <div>{expandHideCaption}</div>
+                <div>{ expandHideCaption }</div>
             </AccessibleButton>);
         } else {
             expandButton = (<AccessibleButton className="mx_UserInfo_expand mx_linkButton"
                 onClick={() => setExpanded(true)}
             >
                 <div className={expandIconClasses} />
-                <div>{expandCountCaption}</div>
+                <div>{ expandCountCaption }</div>
             </AccessibleButton>);
         }
     }
@@ -306,8 +306,8 @@ function DevicesSection({ devices, userId, loading }: {devices: IDevice[], userI
 
     return (
         <div className="mx_UserInfo_devices">
-            <div>{deviceList}</div>
-            <div>{expandButton}</div>
+            <div>{ deviceList }</div>
+            <div>{ expandButton }</div>
         </div>
     );
 }
@@ -385,7 +385,7 @@ const UserOptionsSection: React.FC<{
             }
 
             insertPillButton = (
-                <AccessibleButton onClick={onInsertPillButton} className={"mx_UserInfo_field"}>
+                <AccessibleButton onClick={onInsertPillButton} className="mx_UserInfo_field">
                     { _t('Mention') }
                 </AccessibleButton>
             );
@@ -1038,7 +1038,7 @@ const PowerLevelSection: React.FC<{
         const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
         return (
             <div className="mx_UserInfo_profileField">
-                <div className="mx_UserInfo_roleDescription">{role}</div>
+                <div className="mx_UserInfo_roleDescription">{ role }</div>
             </div>
         );
     }
@@ -1267,7 +1267,7 @@ const BasicUserInfo: React.FC<{
     if (isSynapseAdmin && member.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
         synapseDeactivateButton = (
             <AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field mx_UserInfo_destructive">
-                {_t("Deactivate user")}
+                { _t("Deactivate user") }
             </AccessibleButton>
         );
     }
@@ -1360,7 +1360,7 @@ const BasicUserInfo: React.FC<{
                         legacyVerifyUser(member as User);
                     }
                 }}>
-                    {_t("Verify")}
+                    { _t("Verify") }
                 </AccessibleButton>
             );
         } else if (!showDeviceListSpinner) {
@@ -1518,8 +1518,8 @@ const UserInfoHeader: React.FC<{
                 </div>
                 <div>{ member.userId }</div>
                 <div className="mx_UserInfo_profileStatus">
-                    {presenceLabel}
-                    {statusLabel}
+                    { presenceLabel }
+                    { statusLabel }
                 </div>
             </div>
         </div>
diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx
index a4d4d2fa30..395bdc21e0 100644
--- a/src/components/views/right_panel/VerificationPanel.tsx
+++ b/src/components/views/right_panel/VerificationPanel.tsx
@@ -85,12 +85,12 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
         const brand = SdkConfig.get().brand;
 
         const noCommonMethodError: JSX.Element = !showSAS && !showQR ?
-            <p>{_t(
+            <p>{ _t(
                 "The session you are trying to verify doesn't support scanning a " +
                 "QR code or emoji verification, which is what %(brand)s supports. Try " +
                 "with a different client.",
                 { brand },
-            )}</p> :
+            ) }</p> :
             null;
 
         if (this.props.layout === 'dialog') {
@@ -100,31 +100,31 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
             if (showQR) {
                 qrBlockDialog =
                     <div className='mx_VerificationPanel_QRPhase_startOption'>
-                        <p>{_t("Scan this unique code")}</p>
+                        <p>{ _t("Scan this unique code") }</p>
                         <VerificationQRCode qrCodeData={request.qrCodeData} />
                     </div>;
             }
             if (showSAS) {
                 sasBlockDialog = <div className='mx_VerificationPanel_QRPhase_startOption'>
-                    <p>{_t("Compare unique emoji")}</p>
+                    <p>{ _t("Compare unique emoji") }</p>
                     <span className='mx_VerificationPanel_QRPhase_helpText'>
-                        {_t("Compare a unique set of emoji if you don't have a camera on either device")}
+                        { _t("Compare a unique set of emoji if you don't have a camera on either device") }
                     </span>
                     <AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this.startSAS} kind='primary'>
-                        {_t("Start")}
+                        { _t("Start") }
                     </AccessibleButton>
                 </div>;
             }
             const or = qrBlockDialog && sasBlockDialog ?
-                <div className='mx_VerificationPanel_QRPhase_betweenText'>{_t("or")}</div> : null;
+                <div className='mx_VerificationPanel_QRPhase_betweenText'>{ _t("or") }</div> : null;
             return (
                 <div>
-                    {_t("Verify this session by completing one of the following:")}
+                    { _t("Verify this session by completing one of the following:") }
                     <div className='mx_VerificationPanel_QRPhase_startOptions'>
-                        {qrBlockDialog}
-                        {or}
-                        {sasBlockDialog}
-                        {noCommonMethodError}
+                        { qrBlockDialog }
+                        { or }
+                        { sasBlockDialog }
+                        { noCommonMethodError }
                     </div>
                 </div>
             );
@@ -133,10 +133,10 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
         let qrBlock: JSX.Element;
         if (showQR) {
             qrBlock = <div className="mx_UserInfo_container">
-                <h3>{_t("Verify by scanning")}</h3>
-                <p>{_t("Ask %(displayName)s to scan your code:", {
+                <h3>{ _t("Verify by scanning") }</h3>
+                <p>{ _t("Ask %(displayName)s to scan your code:", {
                     displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
-                })}</p>
+                }) }</p>
 
                 <div className="mx_VerificationPanel_qrCode">
                     <VerificationQRCode qrCodeData={request.qrCodeData} />
@@ -153,28 +153,28 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
 
             // Note: mx_VerificationPanel_verifyByEmojiButton is for the end-to-end tests
             sasBlock = <div className="mx_UserInfo_container">
-                <h3>{_t("Verify by emoji")}</h3>
-                <p>{sasLabel}</p>
+                <h3>{ _t("Verify by emoji") }</h3>
+                <p>{ sasLabel }</p>
                 <AccessibleButton
                     disabled={disabled}
                     kind="primary"
                     className="mx_UserInfo_wideButton mx_VerificationPanel_verifyByEmojiButton"
                     onClick={this.startSAS}
                 >
-                    {_t("Verify by emoji")}
+                    { _t("Verify by emoji") }
                 </AccessibleButton>
             </div>;
         }
 
         const noCommonMethodBlock = noCommonMethodError ?
-            <div className="mx_UserInfo_container">{noCommonMethodError}</div> :
+            <div className="mx_UserInfo_container">{ noCommonMethodError }</div> :
             null;
 
         // TODO: add way to open camera to scan a QR code
         return <React.Fragment>
-            {qrBlock}
-            {sasBlock}
-            {noCommonMethodBlock}
+            { qrBlock }
+            { sasBlock }
+            { noCommonMethodBlock }
         </React.Fragment>;
     }
 
@@ -204,7 +204,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
         if (this.state.reciprocateQREvent) {
             // Element Web doesn't support scanning yet, so assume here we're the client being scanned.
             body = <React.Fragment>
-                <p>{description}</p>
+                <p>{ description }</p>
                 <E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
                 <div className="mx_VerificationPanel_reciprocateButtons">
                     <AccessibleButton
@@ -227,7 +227,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
             body = <p><Spinner /></p>;
         }
         return <div className="mx_UserInfo_container mx_VerificationPanel_reciprocate_section">
-            <h3>{_t("Verify by scanning")}</h3>
+            <h3>{ _t("Verify by scanning") }</h3>
             { body }
         </div>;
     }
@@ -266,12 +266,12 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
 
         return (
             <div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
-                <h3>{_t("Verified")}</h3>
-                <p>{description}</p>
+                <h3>{ _t("Verified") }</h3>
+                <p>{ description }</p>
                 <E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
                 { text ? <p>{ text }</p> : null }
                 <AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
-                    {_t("Got it")}
+                    { _t("Got it") }
                 </AccessibleButton>
             </div>
         );
@@ -305,11 +305,11 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
 
         return (
             <div className="mx_UserInfo_container">
-                <h3>{_t("Verification cancelled")}</h3>
+                <h3>{ _t("Verification cancelled") }</h3>
                 <p>{ text }</p>
 
                 <AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
-                    {_t("Got it")}
+                    { _t("Got it") }
                 </AccessibleButton>
             </div>
         );
@@ -339,7 +339,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
                                 isSelf={request.isSelfVerification}
                             /> : <Spinner />;
                         return <div className="mx_UserInfo_container">
-                            <h3>{_t("Compare emoji")}</h3>
+                            <h3>{ _t("Compare emoji") }</h3>
                             { emojis }
                         </div>;
                     }
diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx
index 4192825e93..9ed49d362b 100644
--- a/src/components/views/room_settings/AliasSettings.tsx
+++ b/src/components/views/room_settings/AliasSettings.tsx
@@ -348,7 +348,7 @@ export default class AliasSettings extends React.Component<IProps, IState> {
                 }
                 {
                     found || !this.state.canonicalAlias ? '' :
-                        <option value={ this.state.canonicalAlias } key='arbitrary'>
+                        <option value={this.state.canonicalAlias} key='arbitrary'>
                             { this.state.canonicalAlias }
                         </option>
                 }
@@ -378,11 +378,11 @@ export default class AliasSettings extends React.Component<IProps, IState> {
 
         return (
             <div className='mx_AliasSettings'>
-                <span className='mx_SettingsTab_subheading'>{_t("Published Addresses")}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t("Published Addresses") }</span>
                 <p>
                     { isSpaceRoom
                         ? _t("Published addresses can be used by anyone on any server to join your space.")
-                        : _t("Published addresses can be used by anyone on any server to join your room.")}
+                        : _t("Published addresses can be used by anyone on any server to join your room.") }
                     &nbsp;
                     { _t("To publish an address, it needs to be set as a local address first.") }
                 </p>
@@ -394,9 +394,9 @@ export default class AliasSettings extends React.Component<IProps, IState> {
                         canSetCanonicalAlias={this.props.canSetCanonicalAlias}
                     /> }
                 <datalist id="mx_AliasSettings_altRecommendations">
-                    {this.getLocalNonAltAliases().map(alias => {
+                    { this.getLocalNonAltAliases().map(alias => {
                         return <option value={alias} key={alias} />;
-                    })};
+                    }) };
                 </datalist>
                 <EditableAliasesList
                     id="roomAltAliases"
@@ -423,7 +423,7 @@ export default class AliasSettings extends React.Component<IProps, IState> {
                         "through your homeserver (%(localDomain)s)", { localDomain }) }
                 </p>
                 <details onToggle={this.onLocalAliasesToggled} open={this.state.detailsOpen}>
-                    <summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
+                    <summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more") }</summary>
                     { localAliasesList }
                 </details>
             </div>
diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js
index f2533bc11e..23b497398a 100644
--- a/src/components/views/room_settings/RelatedGroupSettings.js
+++ b/src/components/views/room_settings/RelatedGroupSettings.js
@@ -106,7 +106,7 @@ export default class RelatedGroupSettings extends React.Component {
             <EditableItemList
                 id="relatedGroups"
                 items={this.state.newGroupsList}
-                className={"mx_RelatedGroupSettings"}
+                className="mx_RelatedGroupSettings"
                 newItem={this.state.newGroupId}
                 canRemove={this.props.canSetRelatedGroups}
                 canEdit={this.props.canSetRelatedGroups}
diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js
index ded186af9c..a1dfbe31dc 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.js
@@ -185,14 +185,14 @@ export default class RoomProfileSettings extends React.Component {
                         kind="link"
                         disabled={!this.state.enableProfileSave}
                     >
-                        {_t("Cancel")}
+                        { _t("Cancel") }
                     </AccessibleButton>
                     <AccessibleButton
                         onClick={this._saveProfile}
                         kind="primary"
                         disabled={!this.state.enableProfileSave}
                     >
-                        {_t("Save")}
+                        { _t("Save") }
                     </AccessibleButton>
                 </div>
             );
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index f142328895..4a62d6711e 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -179,9 +179,9 @@ export default class AuxPanel extends React.Component<IProps, IState> {
                     <span
                         className="m_RoomView_auxPanel_stateViews_span"
                         data-severity={severity}
-                        key={ "x-" + stateKey }
+                        key={"x-" + stateKey}
                     >
-                        {span}
+                        { span }
                     </span>
                 );
 
diff --git a/src/components/views/rooms/EntityTile.tsx b/src/components/views/rooms/EntityTile.tsx
index 74738c3683..88c54468d8 100644
--- a/src/components/views/rooms/EntityTile.tsx
+++ b/src/components/views/rooms/EntityTile.tsx
@@ -129,23 +129,23 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
                     presenceState={this.props.presenceState} />;
             }
             if (this.props.subtextLabel) {
-                presenceLabel = <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>;
+                presenceLabel = <span className="mx_EntityTile_subtext">{ this.props.subtextLabel }</span>;
             }
             nameEl = (
                 <div className="mx_EntityTile_details">
                     <div className="mx_EntityTile_name" dir="auto">
                         { name }
                     </div>
-                    {presenceLabel}
+                    { presenceLabel }
                 </div>
             );
         } else if (this.props.subtextLabel) {
             nameEl = (
                 <div className="mx_EntityTile_details">
                     <div className="mx_EntityTile_name" dir="auto">
-                        {name}
+                        { name }
                     </div>
-                    <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
+                    <span className="mx_EntityTile_subtext">{ this.props.subtextLabel }</span>
                 </div>
             );
         } else {
@@ -167,7 +167,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
         const powerStatus = this.props.powerStatus;
         if (powerStatus) {
             const powerText = PowerLabel[powerStatus];
-            powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
+            powerLabel = <div className="mx_EntityTile_power">{ powerText }</div>;
         }
 
         let e2eIcon;
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 1bdcccd77f..6861ea7af5 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -44,6 +44,7 @@ import EditorStateTransfer from "../../../utils/EditorStateTransfer";
 import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
 import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
 import NotificationBadge from "./NotificationBadge";
+import CallEventGrouper from "../../structures/CallEventGrouper";
 import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
 import { Action } from '../../../dispatcher/actions';
 import MemberAvatar from '../avatars/MemberAvatar';
@@ -60,10 +61,7 @@ const eventTileTypes = {
     [EventType.Sticker]: 'messages.MessageEvent',
     [EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion',
     [EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion',
-    [EventType.CallInvite]: 'messages.TextualEvent',
-    [EventType.CallAnswer]: 'messages.TextualEvent',
-    [EventType.CallHangup]: 'messages.TextualEvent',
-    [EventType.CallReject]: 'messages.TextualEvent',
+    [EventType.CallInvite]: 'messages.CallEvent',
 };
 
 const stateEventTileTypes = {
@@ -170,8 +168,6 @@ export function getHandlerTile(ev) {
     return eventTileTypes[type];
 }
 
-const MAX_READ_AVATARS = 5;
-
 // Our component structure for EventTiles on the timeline is:
 //
 // .-EventTile------------------------------------------------.
@@ -292,11 +288,17 @@ interface IProps {
     // Helper to build permalinks for the room
     permalinkCreator?: RoomPermalinkCreator;
 
+    // CallEventGrouper for this event
+    callEventGrouper?: CallEventGrouper;
+
     // Symbol of the root node
     as?: string;
 
     // whether or not to always show timestamps
     alwaysShowTimestamps?: boolean;
+
+    // whether or not to display the sender
+    hideSender?: boolean;
 }
 
 interface IState {
@@ -430,7 +432,7 @@ export default class EventTile extends React.Component<IProps, IState> {
     }
 
     // TODO: [REACT-WARNING] Move into constructor
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     UNSAFE_componentWillMount() {
         this.verifyEvent(this.props.mxEvent);
     }
@@ -452,7 +454,7 @@ export default class EventTile extends React.Component<IProps, IState> {
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     UNSAFE_componentWillReceiveProps(nextProps) {
         // re-check the sender verification as outgoing events progress through
         // the send process.
@@ -656,6 +658,10 @@ export default class EventTile extends React.Component<IProps, IState> {
             return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
         }
 
+        const MAX_READ_AVATARS = this.props.layout == Layout.Bubble
+            ? 2
+            : 5;
+
         // return early if there are no read receipts
         if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
             // We currently must include `mx_EventTile_readAvatars` in the DOM
@@ -951,7 +957,7 @@ export default class EventTile extends React.Component<IProps, IState> {
             );
         }
 
-        if (needsSenderProfile) {
+        if (needsSenderProfile && this.props.hideSender !== true) {
             if (!this.props.tileShape) {
                 sender = <SenderProfile onClick={this.onSenderProfileClick}
                     mxEvent={this.props.mxEvent}
@@ -971,8 +977,12 @@ export default class EventTile extends React.Component<IProps, IState> {
             onFocusChange={this.onActionBarFocusChange}
         /> : undefined;
 
-        const showTimestamp = this.props.mxEvent.getTs() &&
-            (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused);
+        const showTimestamp = this.props.mxEvent.getTs()
+            && (this.props.alwaysShowTimestamps
+            || this.props.last
+            || this.state.hover
+            || this.state.actionBarFocused);
+
         const timestamp = showTimestamp ?
             <MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
 
@@ -1112,6 +1122,8 @@ export default class EventTile extends React.Component<IProps, IState> {
                     this.props.alwaysShowTimestamps || this.state.hover,
                 );
 
+                const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
+
                 // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
                 return (
                     React.createElement(this.props.as || "li", {
@@ -1121,6 +1133,9 @@ export default class EventTile extends React.Component<IProps, IState> {
                         "aria-live": ariaLive,
                         "aria-atomic": "true",
                         "data-scroll-tokens": scrollToken,
+                        "data-layout": this.props.layout,
+                        "data-self": isOwnEvent,
+                        "data-has-reply": !!thread,
                         "onMouseEnter": () => this.setState({ hover: true }),
                         "onMouseLeave": () => this.setState({ hover: false }),
                     }, <>
@@ -1140,11 +1155,12 @@ export default class EventTile extends React.Component<IProps, IState> {
                                 showUrlPreview={this.props.showUrlPreview}
                                 permalinkCreator={this.props.permalinkCreator}
                                 onHeightChanged={this.props.onHeightChanged}
+                                callEventGrouper={this.props.callEventGrouper}
                             />
                             { keyRequestInfo }
-                            { reactionsRow }
                             { actionBar }
                         </div>
+                        { reactionsRow }
                         { msgOption }
                         { avatar }
                     </>)
@@ -1250,7 +1266,7 @@ class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> {
                 className={classes}
                 onMouseEnter={this.onHoverStart}
                 onMouseLeave={this.onHoverEnd}
-            >{tooltip}</div>
+            >{ tooltip }</div>
         );
     }
 }
@@ -1314,8 +1330,8 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
             <div className="mx_EventTile_msgOption">
                 <span className="mx_EventTile_readAvatars">
                     <span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
-                        {nonCssBadge}
-                        {tooltip}
+                        { nonCssBadge }
+                        { tooltip }
                     </span>
                 </span>
             </div>
diff --git a/src/components/views/rooms/ExtraTile.tsx b/src/components/views/rooms/ExtraTile.tsx
index 18c5d50ae8..e74dde3a0f 100644
--- a/src/components/views/rooms/ExtraTile.tsx
+++ b/src/components/views/rooms/ExtraTile.tsx
@@ -84,7 +84,7 @@ export default class ExtraTile extends React.Component<IProps, IState> {
         let nameContainer = (
             <div className="mx_RoomTile_nameContainer">
                 <div title={name} className={nameClasses} tabIndex={-1} dir="auto">
-                    {name}
+                    { name }
                 </div>
             </div>
         );
@@ -106,11 +106,11 @@ export default class ExtraTile extends React.Component<IProps, IState> {
                     title={this.props.isMinimized ? name : undefined}
                 >
                     <div className="mx_RoomTile_avatarContainer">
-                        {this.props.avatar}
+                        { this.props.avatar }
                     </div>
-                    {nameContainer}
+                    { nameContainer }
                     <div className="mx_RoomTile_badgeContainer">
-                        {badge}
+                        { badge }
                     </div>
                 </Button>
             </React.Fragment>
diff --git a/src/components/views/rooms/JumpToBottomButton.js b/src/components/views/rooms/JumpToBottomButton.js
index b6cefc1231..15872bdeb0 100644
--- a/src/components/views/rooms/JumpToBottomButton.js
+++ b/src/components/views/rooms/JumpToBottomButton.js
@@ -25,7 +25,7 @@ export default (props) => {
     });
     let badge;
     if (props.numUnreadMessages) {
-        badge = (<div className="mx_JumpToBottomButton_badge">{props.numUnreadMessages}</div>);
+        badge = (<div className="mx_JumpToBottomButton_badge">{ props.numUnreadMessages }</div>);
     }
     return (<div className={className}>
         <AccessibleButton className="mx_JumpToBottomButton_scrollDown"
diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx
index 71e54404c0..90a2a03c12 100644
--- a/src/components/views/rooms/MemberList.tsx
+++ b/src/components/views/rooms/MemberList.tsx
@@ -93,7 +93,7 @@ export default class MemberList extends React.Component<IProps, IState> {
         this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
     }
 
-    // eslint-disable-next-line camelcase
+    // eslint-disable-next-line
     UNSAFE_componentWillMount() {
         const cli = MatrixClientPeg.get();
         this.mounted = true;
@@ -543,8 +543,8 @@ export default class MemberList extends React.Component<IProps, IState> {
         const footer = (
             <SearchBox
                 className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
-                placeholder={ _t('Filter room members') }
-                onSearch={ this.onSearchQueryChanged } />
+                placeholder={_t('Filter room members')}
+                onSearch={this.onSearchQueryChanged} />
         );
 
         let previousPhase = RightPanelPhases.RoomSummary;
diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx
index b7015d2275..b16d22b416 100644
--- a/src/components/views/rooms/MessageComposer.tsx
+++ b/src/components/views/rooms/MessageComposer.tsx
@@ -411,7 +411,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
                     className="mx_MessageComposer_roomReplaced_link"
                     onClick={this.onTombstoneClick}
                 >
-                    {_t("The conversation continues here.")}
+                    { _t("The conversation continues here.") }
                 </a>
             ) : '';
 
@@ -421,7 +421,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
                         src={require("../../../../res/img/room_replaced.svg")}
                     />
                     <span className="mx_MessageComposer_roomReplaced_header">
-                        {_t("This room has been replaced and is no longer active.")}
+                        { _t("This room has been replaced and is no longer active.") }
                     </span><br />
                     { continuesLink }
                 </div>
@@ -445,7 +445,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
 
         return (
             <div className="mx_MessageComposer mx_GroupLayout">
-                {recordingTooltip}
+                { recordingTooltip }
                 <div className="mx_MessageComposer_wrapper">
                     <ReplyPreview permalinkCreator={this.props.permalinkCreator} />
                     <div className="mx_MessageComposer_row">
diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 8961bcc253..a41ad19b41 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -68,9 +68,9 @@ const NewRoomIntro = () => {
 
             <h2>{ room.name }</h2>
 
-            <p>{_t("This is the beginning of your direct message history with <displayName/>.", {}, {
+            <p>{ _t("This is the beginning of your direct message history with <displayName/>.", {}, {
                 displayName: () => <b>{ displayName }</b>,
-            })}</p>
+            }) }</p>
             { caption && <p>{ caption }</p> }
         </React.Fragment>;
     } else {
@@ -132,7 +132,7 @@ const NewRoomIntro = () => {
                         showSpaceInvite(parentSpace);
                     }}
                 >
-                    {_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}
+                    { _t("Invite to %(spaceName)s", { spaceName: parentSpace.name }) }
                 </AccessibleButton>
                 { room.canInvite(cli.getUserId()) && <AccessibleButton
                     className="mx_NewRoomIntro_inviteButton"
@@ -141,7 +141,7 @@ const NewRoomIntro = () => {
                         dis.dispatch({ action: "view_invite", roomId });
                     }}
                 >
-                    {_t("Invite to just this room")}
+                    { _t("Invite to just this room") }
                 </AccessibleButton> }
             </div>;
         } else if (room.canInvite(cli.getUserId())) {
@@ -153,7 +153,7 @@ const NewRoomIntro = () => {
                         dis.dispatch({ action: "view_invite", roomId });
                     }}
                 >
-                    {_t("Invite to this room")}
+                    { _t("Invite to this room") }
                 </AccessibleButton>
             </div>;
         }
@@ -170,10 +170,10 @@ const NewRoomIntro = () => {
 
             <h2>{ room.name }</h2>
 
-            <p>{createdText} {_t("This is the start of <roomName/>.", {}, {
+            <p>{ createdText } { _t("This is the start of <roomName/>.", {}, {
                 roomName: () => <b>{ room.name }</b>,
-            })}</p>
-            <p>{topicText}</p>
+            }) }</p>
+            <p>{ topicText }</p>
             { buttons }
         </React.Fragment>;
     }
@@ -190,7 +190,7 @@ const NewRoomIntro = () => {
         "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> },
+        { a: sub => <a onClick={openRoomSettings} href="#">{ sub }</a> },
     );
 
     return <div className="mx_NewRoomIntro">
@@ -201,7 +201,7 @@ const NewRoomIntro = () => {
                 title={_t("End-to-end encryption isn't enabled")}
                 subtitle={sub2}
             />
-        )}
+        ) }
 
         { body }
     </div>;
diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx
index 778a8a7215..8329de7391 100644
--- a/src/components/views/rooms/NotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge.tsx
@@ -126,14 +126,14 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
         if (onClick) {
             return (
                 <AccessibleButton {...props} className={classes} onClick={onClick}>
-                    <span className="mx_NotificationBadge_count">{symbol}</span>
+                    <span className="mx_NotificationBadge_count">{ symbol }</span>
                 </AccessibleButton>
             );
         }
 
         return (
             <div className={classes}>
-                <span className="mx_NotificationBadge_count">{symbol}</span>
+                <span className="mx_NotificationBadge_count">{ symbol }</span>
             </div>
         );
     }
diff --git a/src/components/views/rooms/PinnedEventTile.tsx b/src/components/views/rooms/PinnedEventTile.tsx
index 0e3396e9b0..250efc1278 100644
--- a/src/components/views/rooms/PinnedEventTile.tsx
+++ b/src/components/views/rooms/PinnedEventTile.tsx
@@ -85,6 +85,7 @@ export default class PinnedEventTile extends React.Component<IProps> {
             <div className="mx_PinnedEventTile_message">
                 <MessageEvent
                     mxEvent={this.props.event}
+                    // @ts-ignore - complaining that className is invalid when it's not
                     className="mx_PinnedEventTile_body"
                     maxImageHeight={150}
                     onHeightChanged={() => {}} // we need to give this, apparently
diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx
index 89f004ffc1..2e133b0487 100644
--- a/src/components/views/rooms/RoomBreadcrumbs.tsx
+++ b/src/components/views/rooms/RoomBreadcrumbs.tsx
@@ -109,7 +109,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
                     classNames='mx_RoomBreadcrumbs'
                 >
                     <Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>
-                        {tiles.slice(this.state.skipFirst ? 1 : 0)}
+                        { tiles.slice(this.state.skipFirst ? 1 : 0) }
                     </Toolbar>
                 </CSSTransition>
             );
@@ -117,7 +117,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
             return (
                 <div className='mx_RoomBreadcrumbs'>
                     <div className="mx_RoomBreadcrumbs_placeholder">
-                        {_t("No recently visited rooms")}
+                        { _t("No recently visited rooms") }
                     </div>
                 </div>
             );
diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx
index af5daed5bc..c79b5bddd5 100644
--- a/src/components/views/rooms/RoomHeader.tsx
+++ b/src/components/views/rooms/RoomHeader.tsx
@@ -121,18 +121,18 @@ export default class RoomHeader extends React.Component<IProps> {
         const name =
             <div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
                 <RoomName room={this.props.room}>
-                    {(name) => {
+                    { (name) => {
                         const roomName = name || oobName;
                         return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
-                    }}
+                    } }
                 </RoomName>
                 { searchStatus }
             </div>;
 
         const topicElement = <RoomTopic room={this.props.room}>
-            {(topic, ref) => <div className="mx_RoomHeader_topic" ref={ref} title={topic} dir="auto">
+            { (topic, ref) => <div className="mx_RoomHeader_topic" ref={ref} title={topic} dir="auto">
                 { topic }
-            </div>}
+            </div> }
         </RoomTopic>;
 
         let roomAvatar;
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 7ece6add9c..84521158df 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -507,13 +507,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
         if (!this.props.isMinimized) {
             if (this.state.isNameFiltering) {
                 explorePrompt = <div className="mx_RoomList_explorePrompt">
-                    <div>{_t("Can't see what you’re looking for?")}</div>
+                    <div>{ _t("Can't see what you’re looking for?") }</div>
                     <AccessibleButton
                         className="mx_RoomList_explorePrompt_startChat"
                         kind="link"
                         onClick={this.onStartChat}
                     >
-                        {_t("Start a new chat")}
+                        { _t("Start a new chat") }
                     </AccessibleButton>
                     <AccessibleButton
                         className="mx_RoomList_explorePrompt_explore"
@@ -532,13 +532,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
                         className="mx_RoomList_explorePrompt_spaceInvite"
                         onClick={this.onSpaceInviteClick}
                     >
-                        {_t("Invite people")}
+                        { _t("Invite people") }
                     </AccessibleButton> }
                     { this.props.activeSpace.getMyMembership() === "join" && <AccessibleButton
                         className="mx_RoomList_explorePrompt_spaceExplore"
                         onClick={this.onExplore}
                     >
-                        {_t("Explore rooms")}
+                        { _t("Explore rooms") }
                     </AccessibleButton> }
                 </div>;
             } else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
@@ -549,20 +549,20 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
                 // show a prompt to join/create rooms if the user is in 0 rooms and no historical
                 if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
                     explorePrompt = <div className="mx_RoomList_explorePrompt">
-                        <div>{_t("Use the + to make a new room or explore existing ones below")}</div>
+                        <div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
                         <AccessibleButton
                             className="mx_RoomList_explorePrompt_startChat"
                             kind="link"
                             onClick={this.onStartChat}
                         >
-                            {_t("Start a new chat")}
+                            { _t("Start a new chat") }
                         </AccessibleButton>
                         <AccessibleButton
                             className="mx_RoomList_explorePrompt_explore"
                             kind="link"
                             onClick={this.onExplore}
                         >
-                            {_t("Explore all public rooms")}
+                            { _t("Explore all public rooms") }
                         </AccessibleButton>
                     </div>;
                 }
@@ -572,7 +572,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
         const sublists = this.renderSublists();
         return (
             <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
-                {({ onKeyDownHandler }) => (
+                { ({ onKeyDownHandler }) => (
                     <div
                         onFocus={this.props.onFocus}
                         onBlur={this.props.onBlur}
@@ -581,10 +581,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
                         role="tree"
                         aria-label={_t("Rooms")}
                     >
-                        {sublists}
-                        {explorePrompt}
+                        { sublists }
+                        { explorePrompt }
                     </div>
-                )}
+                ) }
             </RovingTabIndexProvider>
         );
     }
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js
index 155b7ffe63..3cd34b1966 100644
--- a/src/components/views/rooms/RoomPreviewBar.js
+++ b/src/components/views/rooms/RoomPreviewBar.js
@@ -340,7 +340,7 @@ export default class RoomPreviewBar extends React.Component {
                     footer = (
                         <div>
                             <Spinner w={20} h={20} />
-                            {_t("Loading room preview")}
+                            { _t("Loading room preview") }
                         </div>
                     );
                 }
@@ -465,11 +465,11 @@ export default class RoomPreviewBar extends React.Component {
                 if (inviteMember) {
                     inviterElement = <span>
                         <span className="mx_RoomPreviewBar_inviter">
-                            {inviteMember.rawDisplayName}
-                        </span> ({inviteMember.userId})
+                            { inviteMember.rawDisplayName }
+                        </span> ({ inviteMember.userId })
                     </span>;
                 } else {
-                    inviterElement = (<span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>);
+                    inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
                 }
 
                 const isDM = this._isDMInvite();
@@ -549,7 +549,7 @@ export default class RoomPreviewBar extends React.Component {
             if (!Array.isArray(subTitle)) {
                 subTitle = [subTitle];
             }
-            subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{t}</p>);
+            subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{ t }</p>);
         }
 
         let titleElement;
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index 8d825a2b53..cf82040793 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -574,20 +574,20 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                     <React.Fragment>
                         <hr />
                         <div>
-                            <div className='mx_RoomSublist_contextMenu_title'>{_t("Appearance")}</div>
+                            <div className='mx_RoomSublist_contextMenu_title'>{ _t("Appearance") }</div>
                             <StyledMenuItemCheckbox
                                 onClose={this.onCloseMenu}
                                 onChange={this.onUnreadFirstChanged}
                                 checked={isUnreadFirst}
                             >
-                                {_t("Show rooms with unread messages first")}
+                                { _t("Show rooms with unread messages first") }
                             </StyledMenuItemCheckbox>
                             <StyledMenuItemCheckbox
                                 onClose={this.onCloseMenu}
                                 onChange={this.onMessagePreviewChanged}
                                 checked={this.layout.showPreviews}
                             >
-                                {_t("Show previews of messages")}
+                                { _t("Show previews of messages") }
                             </StyledMenuItemCheckbox>
                         </div>
                     </React.Fragment>
@@ -603,14 +603,14 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                 >
                     <div className="mx_RoomSublist_contextMenu">
                         <div>
-                            <div className='mx_RoomSublist_contextMenu_title'>{_t("Sort by")}</div>
+                            <div className='mx_RoomSublist_contextMenu_title'>{ _t("Sort by") }</div>
                             <StyledMenuItemRadio
                                 onClose={this.onCloseMenu}
                                 onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
                                 checked={!isAlphabetical}
                                 name={`mx_${this.props.tagId}_sortBy`}
                             >
-                                {_t("Activity")}
+                                { _t("Activity") }
                             </StyledMenuItemRadio>
                             <StyledMenuItemRadio
                                 onClose={this.onCloseMenu}
@@ -618,10 +618,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                                 checked={isAlphabetical}
                                 name={`mx_${this.props.tagId}_sortBy`}
                             >
-                                {_t("A-Z")}
+                                { _t("A-Z") }
                             </StyledMenuItemRadio>
                         </div>
-                        {otherSections}
+                        { otherSections }
                     </div>
                 </ContextMenu>
             );
@@ -634,7 +634,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                     onFinished={this.onCloseAddRoomMenu}
                     compact
                 >
-                    {this.props.addRoomContextMenu(this.onCloseAddRoomMenu)}
+                    { this.props.addRoomContextMenu(this.onCloseAddRoomMenu) }
                 </IconizedContextMenu>
             );
         }
@@ -647,7 +647,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                     title={_t("List options")}
                     isExpanded={!!this.state.contextMenuPosition}
                 />
-                {contextMenu}
+                { contextMenu }
             </React.Fragment>
         );
     }
@@ -655,7 +655,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
     private renderHeader(): React.ReactElement {
         return (
             <RovingTabIndexWrapper inputRef={this.headerButton}>
-                {({ onFocus, isActive, ref }) => {
+                { ({ onFocus, isActive, ref }) => {
                     const tabIndex = isActive ? 0 : -1;
 
                     let ariaLabel = _t("Jump to first unread room.");
@@ -711,7 +711,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
 
                     const badgeContainer = (
                         <div className="mx_RoomSublist_badgeContainer">
-                            {badge}
+                            { badge }
                         </div>
                     );
 
@@ -746,17 +746,17 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                                     title={this.props.isMinimized ? this.props.label : undefined}
                                 >
                                     <span className={collapseClasses} />
-                                    <span>{this.props.label}</span>
+                                    <span>{ this.props.label }</span>
                                 </Button>
-                                {this.renderMenu()}
-                                {this.props.isMinimized ? null : badgeContainer}
-                                {this.props.isMinimized ? null : addRoomButton}
+                                { this.renderMenu() }
+                                { this.props.isMinimized ? null : badgeContainer }
+                                { this.props.isMinimized ? null : addRoomButton }
                             </div>
-                            {this.props.isMinimized ? badgeContainer : null}
-                            {this.props.isMinimized ? addRoomButton : null}
+                            { this.props.isMinimized ? badgeContainer : null }
+                            { this.props.isMinimized ? addRoomButton : null }
                         </div>
                     );
-                }}
+                } }
             </RovingTabIndexWrapper>
         );
     }
@@ -804,7 +804,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                 const label = _t("Show %(count)s more", { count: numMissing });
                 let showMoreText = (
                     <span className='mx_RoomSublist_showNButtonText'>
-                        {label}
+                        { label }
                     </span>
                 );
                 if (this.props.isMinimized) showMoreText = null;
@@ -816,9 +816,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                         aria-label={label}
                     >
                         <span className='mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron'>
-                            {/* set by CSS masking */}
+                            { /* set by CSS masking */ }
                         </span>
-                        {showMoreText}
+                        { showMoreText }
                     </RovingAccessibleButton>
                 );
             } else if (this.numTiles > this.layout.defaultVisibleTiles) {
@@ -826,7 +826,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                 const label = _t("Show less");
                 let showLessText = (
                     <span className='mx_RoomSublist_showNButtonText'>
-                        {label}
+                        { label }
                     </span>
                 );
                 if (this.props.isMinimized) showLessText = null;
@@ -838,9 +838,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                         aria-label={label}
                     >
                         <span className='mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron'>
-                            {/* set by CSS masking */}
+                            { /* set by CSS masking */ }
                         </span>
-                        {showLessText}
+                        { showLessText }
                     </RovingAccessibleButton>
                 );
             }
@@ -891,9 +891,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                         enable={handles}
                     >
                         <div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
-                            {visibleTiles}
+                            { visibleTiles }
                         </div>
-                        {showNButton}
+                        { showNButton }
                     </Resizable>
                 </React.Fragment>
             );
@@ -909,8 +909,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                 aria-label={this.props.label}
                 onKeyDown={this.onKeyDown}
             >
-                {this.renderHeader()}
-                {content}
+                { this.renderHeader() }
+                { content }
             </div>
         );
     }
diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx
index 0f614d5337..ae4569fbaf 100644
--- a/src/components/views/rooms/RoomTile.tsx
+++ b/src/components/views/rooms/RoomTile.tsx
@@ -467,7 +467,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
                     isExpanded={!!this.state.notificationsMenuPosition}
                     tabIndex={isActive ? 0 : -1}
                 />
-                {contextMenu}
+                { contextMenu }
             </React.Fragment>
         );
     }
@@ -521,13 +521,13 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
                         label={lowPriorityLabel}
                         iconClassName="mx_RoomTile_iconArrowDown"
                     />
-                    {canInvite ? (
+                    { canInvite ? (
                         <IconizedContextMenuOption
                             onClick={this.onInviteClick}
                             label={_t("Invite People")}
                             iconClassName="mx_RoomTile_iconInvite"
                         />
-                    ) : null}
+                    ) : null }
                     <IconizedContextMenuOption
                         onClick={this.onCopyRoomClick}
                         label={_t("Copy Room Link")}
@@ -557,7 +557,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
                     title={_t("Room options")}
                     isExpanded={!!this.state.generalMenuPosition}
                 />
-                {contextMenu}
+                { contextMenu }
             </React.Fragment>
         );
     }
@@ -621,7 +621,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
                     id={messagePreviewId(this.props.room.roomId)}
                     title={this.state.messagePreview}
                 >
-                    {this.state.messagePreview}
+                    { this.state.messagePreview }
                 </div>
             );
         }
@@ -635,9 +635,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
         let nameContainer = (
             <div className="mx_RoomTile_nameContainer">
                 <div title={name} className={nameClasses} tabIndex={-1} dir="auto">
-                    {name}
+                    { name }
                 </div>
-                {messagePreview}
+                { messagePreview }
             </div>
         );
         if (this.props.isMinimized) nameContainer = null;
@@ -675,7 +675,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
         return (
             <React.Fragment>
                 <RovingTabIndexWrapper inputRef={this.roomTileRef}>
-                    {({ onFocus, isActive, ref }) =>
+                    { ({ onFocus, isActive, ref }) =>
                         <Button
                             {...props}
                             onFocus={onFocus}
@@ -689,11 +689,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
                             aria-selected={this.state.selected}
                             aria-describedby={ariaDescribedBy}
                         >
-                            {roomAvatar}
-                            {nameContainer}
-                            {badge}
-                            {this.renderGeneralMenu()}
-                            {this.renderNotificationsMenu(isActive)}
+                            { roomAvatar }
+                            { nameContainer }
+                            { badge }
+                            { this.renderGeneralMenu() }
+                            { this.renderNotificationsMenu(isActive) }
                         </Button>
                     }
                 </RovingTabIndexWrapper>
diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js
index 820e8075d7..384845cdf9 100644
--- a/src/components/views/rooms/RoomUpgradeWarningBar.js
+++ b/src/components/views/rooms/RoomUpgradeWarningBar.js
@@ -72,26 +72,26 @@ export default class RoomUpgradeWarningBar extends React.PureComponent {
             <div>
                 <div className="mx_RoomUpgradeWarningBar_body">
                     <p>
-                        {_t(
+                        { _t(
                             "Upgrading this room will shut down the current instance of the room and create " +
                             "an upgraded room with the same name.",
-                        )}
+                        ) }
                     </p>
                     <p>
-                        {_t(
+                        { _t(
                             "<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": (sub) => <b>{sub}</b>,
-                                "i": (sub) => <i>{sub}</i>,
+                                "b": (sub) => <b>{ sub }</b>,
+                                "i": (sub) => <i>{ sub }</i>,
                             },
-                        )}
+                        ) }
                     </p>
                 </div>
                 <p className="mx_RoomUpgradeWarningBar_upgradelink">
                     <AccessibleButton onClick={this.onUpgradeClick}>
-                        {_t("Upgrade this room to the recommended room version")}
+                        { _t("Upgrade this room to the recommended room version") }
                     </AccessibleButton>
                 </p>
             </div>
@@ -101,7 +101,7 @@ export default class RoomUpgradeWarningBar extends React.PureComponent {
             doUpgradeWarnings = (
                 <div className="mx_RoomUpgradeWarningBar_body">
                     <p>
-                        {_t("This room has already been upgraded.")}
+                        { _t("This room has already been upgraded.") }
                     </p>
                 </div>
             );
@@ -111,19 +111,19 @@ export default class RoomUpgradeWarningBar extends React.PureComponent {
             <div className="mx_RoomUpgradeWarningBar">
                 <div className="mx_RoomUpgradeWarningBar_wrapped">
                     <div className="mx_RoomUpgradeWarningBar_header">
-                        {_t(
+                        { _t(
                             "This room is running room version <roomVersion />, which this homeserver has " +
                             "marked as <i>unstable</i>.",
                             {},
                             {
-                                "roomVersion": () => <code>{this.props.room.getVersion()}</code>,
-                                "i": (sub) => <i>{sub}</i>,
+                                "roomVersion": () => <code>{ this.props.room.getVersion() }</code>,
+                                "i": (sub) => <i>{ sub }</i>,
                             },
-                        )}
+                        ) }
                     </div>
-                    {doUpgradeWarnings}
+                    { doUpgradeWarnings }
                     <div className="mx_RoomUpgradeWarningBar_small">
-                        {_t("Only room administrators will see this warning")}
+                        { _t("Only room administrators will see this warning") }
                     </div>
                 </div>
             </div>
diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx
index d71bb8da73..81d0402050 100644
--- a/src/components/views/rooms/SearchBar.tsx
+++ b/src/components/views/rooms/SearchBar.tsx
@@ -100,7 +100,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
                             aria-checked={this.state.scope === SearchScope.Room}
                             role="radio"
                         >
-                            {_t("This Room")}
+                            { _t("This Room") }
                         </AccessibleButton>
                         <AccessibleButton
                             className={allRoomsClasses}
@@ -108,7 +108,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
                             aria-checked={this.state.scope === SearchScope.All}
                             role="radio"
                         >
-                            {_t("All Rooms")}
+                            { _t("All Rooms") }
                         </AccessibleButton>
                     </div>
                     <div className="mx_SearchBar_input mx_textinput">
@@ -119,7 +119,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
                             placeholder={_t("Search…")}
                             onKeyDown={this.onSearchChange}
                         />
-                        <AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
+                        <AccessibleButton className={searchButtonClasses} onClick={this.onSearch} />
                     </div>
                     <AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
                 </div>
diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx
index 0639c20fef..890d1c284c 100644
--- a/src/components/views/rooms/SendMessageComposer.tsx
+++ b/src/components/views/rooms/SendMessageComposer.tsx
@@ -441,7 +441,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
     }
 
     // TODO: [REACT-WARNING] Move this to constructor
-    UNSAFE_componentWillMount() { // eslint-disable-line camelcase
+    UNSAFE_componentWillMount() { // eslint-disable-line
         const partCreator = new CommandPartCreator(this.props.room, this.context);
         const parts = this.restoreStoredEditorState(partCreator) || [];
         this.model = new EditorModel(parts, partCreator);
diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.tsx b/src/components/views/rooms/ThirdPartyMemberInfo.tsx
index 51bb891c62..1590ce0871 100644
--- a/src/components/views/rooms/ThirdPartyMemberInfo.tsx
+++ b/src/components/views/rooms/ThirdPartyMemberInfo.tsx
@@ -123,10 +123,10 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
         if (this.state.canKick && this.state.invited) {
             adminTools = (
                 <div className="mx_MemberInfo_container">
-                    <h3>{_t("Admin Tools")}</h3>
+                    <h3>{ _t("Admin Tools") }</h3>
                     <div className="mx_MemberInfo_buttons">
                         <AccessibleButton className="mx_MemberInfo_field" onClick={this.onKickClick}>
-                            {_t("Revoke invite")}
+                            { _t("Revoke invite") }
                         </AccessibleButton>
                     </div>
                 </div>
@@ -150,16 +150,16 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
                         onClick={this.onCancel}
                         title={_t('Close')}
                     />
-                    <h2>{this.state.displayName}</h2>
+                    <h2>{ this.state.displayName }</h2>
                 </div>
                 <div className="mx_MemberInfo_container">
                     <div className="mx_MemberInfo_profile">
                         <div className="mx_MemberInfo_profileField">
-                            {_t("Invited by %(sender)s", { sender: this.state.senderName })}
+                            { _t("Invited by %(sender)s", { sender: this.state.senderName }) }
                         </div>
                     </div>
                 </div>
-                {adminTools}
+                { adminTools }
             </div>
         );
     }
diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx
index 709eab82a0..a5a2c4c963 100644
--- a/src/components/views/rooms/VoiceRecordComposerTile.tsx
+++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx
@@ -124,9 +124,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
             Modal.createTrackedDialog('Microphone Access Error', '', ErrorDialog, {
                 title: _t("Unable to access your microphone"),
                 description: <>
-                    <p>{_t(
+                    <p>{ _t(
                         "We were unable to access your microphone. Please check your browser settings and try again.",
-                    )}</p>
+                    ) }</p>
                 </>,
             });
         };
@@ -139,9 +139,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
                 Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
                     title: _t("No microphone found"),
                     description: <>
-                        <p>{_t(
+                        <p>{ _t(
                             "We didn't find a microphone on your device. Please check your settings and try again.",
-                        )}</p>
+                        ) }</p>
                     </>,
                 });
                 return;
@@ -224,9 +224,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
         }
 
         return (<>
-            {deleteButton}
-            {this.renderWaveformArea()}
-            {recordingInfo}
+            { deleteButton }
+            { this.renderWaveformArea() }
+            { recordingInfo }
         </>);
     }
 }
diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx
index 6b42a0a044..65fbb6a7e0 100644
--- a/src/components/views/rooms/WhoIsTypingTile.tsx
+++ b/src/components/views/rooms/WhoIsTypingTile.tsx
@@ -64,8 +64,8 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
     }
 
     componentDidUpdate(_, prevState) {
-        const wasVisible = this._isVisible(prevState);
-        const isVisible = this._isVisible(this.state);
+        const wasVisible = WhoIsTypingTile.isVisible(prevState);
+        const isVisible = WhoIsTypingTile.isVisible(this.state);
         if (this.props.onShown && !wasVisible && isVisible) {
             this.props.onShown();
         } else if (this.props.onHidden && wasVisible && !isVisible) {
@@ -83,12 +83,12 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
         Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
     }
 
-    private _isVisible(state: IState): boolean {
+    private static isVisible(state: IState): boolean {
         return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
     }
 
     public isVisible = (): boolean => {
-        return this._isVisible(this.state);
+        return WhoIsTypingTile.isVisible(this.state);
     };
 
     private onRoomTimeline = (event: MatrixEvent, room: Room): void => {
diff --git a/src/components/views/settings/AvatarSetting.js b/src/components/views/settings/AvatarSetting.js
index 891e45e90e..f22c4f1c85 100644
--- a/src/components/views/settings/AvatarSetting.js
+++ b/src/components/views/settings/AvatarSetting.js
@@ -59,7 +59,7 @@ const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, rem
     let removeAvatarBtn;
     if (avatarUrl && removeAvatar) {
         removeAvatarBtn = <AccessibleButton onClick={removeAvatar} kind="link_sm">
-            {_t("Remove")}
+            { _t("Remove") }
         </AccessibleButton>;
     }
 
@@ -68,13 +68,13 @@ const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, rem
         "mx_AvatarSetting_avatar_hovering": isHovering && uploadAvatar,
     });
     return <div className={avatarClasses}>
-        {avatarElement}
+        { avatarElement }
         <div className="mx_AvatarSetting_hover">
             <div className="mx_AvatarSetting_hoverBg" />
-            <span>{_t("Upload")}</span>
+            <span>{ _t("Upload") }</span>
         </div>
-        {uploadAvatarBtn}
-        {removeAvatarBtn}
+        { uploadAvatarBtn }
+        { removeAvatarBtn }
     </div>;
 };
 
diff --git a/src/components/views/settings/BridgeTile.tsx b/src/components/views/settings/BridgeTile.tsx
index d3a0c5ea13..7228e4b939 100644
--- a/src/components/views/settings/BridgeTile.tsx
+++ b/src/components/views/settings/BridgeTile.tsx
@@ -91,24 +91,24 @@ export default class BridgeTile extends React.PureComponent<IProps> {
 
         let creator = null;
         if (content.creator) {
-            creator = <li>{_t("This bridge was provisioned by <user />.", {}, {
+            creator = <li>{ _t("This bridge was provisioned by <user />.", {}, {
                 user: () => <Pill
                     type={Pill.TYPE_USER_MENTION}
                     room={this.props.room}
                     url={makeUserPermalink(content.creator)}
                     shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
                 />,
-            })}</li>;
+            }) }</li>;
         }
 
-        const bot = <li>{_t("This bridge is managed by <user />.", {}, {
+        const bot = <li>{ _t("This bridge is managed by <user />.", {}, {
             user: () => <Pill
                 type={Pill.TYPE_USER_MENTION}
                 room={this.props.room}
                 url={makeUserPermalink(content.bridgebot)}
                 shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
             />,
-        })}</li>;
+        }) }</li>;
 
         let networkIcon;
 
@@ -119,9 +119,9 @@ export default class BridgeTile extends React.PureComponent<IProps> {
                 width={48}
                 height={48}
                 resizeMethod='crop'
-                name={ protocolName }
-                idName={ protocolName }
-                url={ avatarUrl }
+                name={protocolName}
+                idName={protocolName}
+                url={avatarUrl}
             />;
         } else {
             networkIcon = <div className="noProtocolIcon"></div>;
@@ -129,10 +129,10 @@ export default class BridgeTile extends React.PureComponent<IProps> {
         let networkItem = null;
         if (network) {
             const networkName = network.displayname || network.id;
-            let networkLink = <span>{networkName}</span>;
+            let networkLink = <span>{ networkName }</span>;
             if (typeof network.external_url === "string" && isUrlPermitted(network.external_url)) {
                 networkLink = (
-                    <a href={network.external_url} target="_blank" rel="noreferrer noopener">{networkName}</a>
+                    <a href={network.external_url} target="_blank" rel="noreferrer noopener">{ networkName }</a>
                 );
             }
             networkItem = _t("Workspace: <networkLink/>", {}, {
@@ -140,26 +140,26 @@ export default class BridgeTile extends React.PureComponent<IProps> {
             });
         }
 
-        let channelLink = <span>{channelName}</span>;
+        let channelLink = <span>{ channelName }</span>;
         if (typeof channel.external_url === "string" && isUrlPermitted(channel.external_url)) {
-            channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{channelName}</a>;
+            channelLink = <a href={channel.external_url} target="_blank" rel="noreferrer noopener">{ channelName }</a>;
         }
 
         const id = this.props.ev.getId();
         return (<li key={id}>
             <div className="column-icon">
-                {networkIcon}
+                { networkIcon }
             </div>
             <div className="column-data">
-                <h3>{protocolName}</h3>
+                <h3>{ protocolName }</h3>
                 <p className="workspace-channel-details">
-                    {networkItem}
-                    <span className="channel">{_t("Channel: <channelLink/>", {}, {
+                    { networkItem }
+                    <span className="channel">{ _t("Channel: <channelLink/>", {}, {
                         channelLink: () => channelLink,
-                    })}</span>
+                    }) }</span>
                 </p>
                 <ul className="metadata">
-                    {creator} {bot}
+                    { creator } { bot }
                 </ul>
             </div>
         </li>);
diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js
index f94716317b..3ee1645a87 100644
--- a/src/components/views/settings/ChangePassword.js
+++ b/src/components/views/settings/ChangePassword.js
@@ -99,7 +99,7 @@ export default class ChangePassword extends React.Component {
                         'and re-import them afterwards. ' +
                         'In future this will be improved.',
                     ) }
-                    {' '}
+                    { ' ' }
                     <a href="https://github.com/vector-im/element-web/issues/2671" target="_blank" rel="noreferrer noopener">
                         https://github.com/vector-im/element-web/issues/2671
                     </a>
diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js
index f865d74d7c..8b9d68bfa5 100644
--- a/src/components/views/settings/CrossSigningPanel.js
+++ b/src/components/views/settings/CrossSigningPanel.js
@@ -163,29 +163,29 @@ export default class CrossSigningPanel extends React.PureComponent {
 
         let errorSection;
         if (error) {
-            errorSection = <div className="error">{error.toString()}</div>;
+            errorSection = <div className="error">{ error.toString() }</div>;
         }
 
         let summarisedStatus;
         if (homeserverSupportsCrossSigning === undefined) {
             summarisedStatus = <Spinner />;
         } else if (!homeserverSupportsCrossSigning) {
-            summarisedStatus = <p>{_t(
+            summarisedStatus = <p>{ _t(
                 "Your homeserver does not support cross-signing.",
-            )}</p>;
+            ) }</p>;
         } else if (crossSigningReady) {
-            summarisedStatus = <p>✅ {_t(
+            summarisedStatus = <p>✅ { _t(
                 "Cross-signing is ready for use.",
-            )}</p>;
+            ) }</p>;
         } else if (crossSigningPrivateKeysInStorage) {
-            summarisedStatus = <p>{_t(
+            summarisedStatus = <p>{ _t(
                 "Your account has a cross-signing identity in secret storage, " +
                 "but it is not yet trusted by this session.",
-            )}</p>;
+            ) }</p>;
         } else {
-            summarisedStatus = <p>{_t(
+            summarisedStatus = <p>{ _t(
                 "Cross-signing is not set up.",
-            )}</p>;
+            ) }</p>;
         }
 
         const keysExistAnywhere = (
@@ -209,7 +209,7 @@ export default class CrossSigningPanel extends React.PureComponent {
         if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
             actions.push(
                 <AccessibleButton key="setup" kind="primary" onClick={this._onBootstrapClick}>
-                    {_t("Set up")}
+                    { _t("Set up") }
                 </AccessibleButton>,
             );
         }
@@ -217,7 +217,7 @@ export default class CrossSigningPanel extends React.PureComponent {
         if (keysExistAnywhere) {
             actions.push(
                 <AccessibleButton key="reset" kind="danger" onClick={this._resetCrossSigning}>
-                    {_t("Reset")}
+                    { _t("Reset") }
                 </AccessibleButton>,
             );
         }
@@ -225,44 +225,44 @@ export default class CrossSigningPanel extends React.PureComponent {
         let actionRow;
         if (actions.length) {
             actionRow = <div className="mx_CrossSigningPanel_buttonRow">
-                {actions}
+                { actions }
             </div>;
         }
 
         return (
             <div>
-                {summarisedStatus}
+                { summarisedStatus }
                 <details>
-                    <summary>{_t("Advanced")}</summary>
+                    <summary>{ _t("Advanced") }</summary>
                     <table className="mx_CrossSigningPanel_statusList"><tbody>
                         <tr>
-                            <td>{_t("Cross-signing public keys:")}</td>
-                            <td>{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}</td>
+                            <td>{ _t("Cross-signing public keys:") }</td>
+                            <td>{ crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found") }</td>
                         </tr>
                         <tr>
-                            <td>{_t("Cross-signing private keys:")}</td>
-                            <td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage")}</td>
+                            <td>{ _t("Cross-signing private keys:") }</td>
+                            <td>{ crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage") }</td>
                         </tr>
                         <tr>
-                            <td>{_t("Master private key:")}</td>
-                            <td>{masterPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
+                            <td>{ _t("Master private key:") }</td>
+                            <td>{ masterPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
                         </tr>
                         <tr>
-                            <td>{_t("Self signing private key:")}</td>
-                            <td>{selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
+                            <td>{ _t("Self signing private key:") }</td>
+                            <td>{ selfSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
                         </tr>
                         <tr>
-                            <td>{_t("User signing private key:")}</td>
-                            <td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
+                            <td>{ _t("User signing private key:") }</td>
+                            <td>{ userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally") }</td>
                         </tr>
                         <tr>
-                            <td>{_t("Homeserver feature support:")}</td>
-                            <td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
+                            <td>{ _t("Homeserver feature support:") }</td>
+                            <td>{ homeserverSupportsCrossSigning ? _t("exists") : _t("not found") }</td>
                         </tr>
                     </tbody></table>
                 </details>
-                {errorSection}
-                {actionRow}
+                { errorSection }
+                { actionRow }
             </div>
         );
     }
diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js
index f54009f31c..0f052332ee 100644
--- a/src/components/views/settings/DevicesPanel.js
+++ b/src/components/views/settings/DevicesPanel.js
@@ -213,7 +213,7 @@ export default class DevicesPanel extends React.Component {
         const deleteButton = this.state.deleting ?
             <Spinner w={22} h={22} /> :
             <AccessibleButton onClick={this._onDeleteClick} kind="danger_sm">
-                { _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length })}
+                { _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length }) }
             </AccessibleButton>;
 
         const classes = classNames(this.props.className, "mx_DevicesPanel");
diff --git a/src/components/views/settings/E2eAdvancedPanel.tsx b/src/components/views/settings/E2eAdvancedPanel.tsx
index f8746682d7..ebb778deb4 100644
--- a/src/components/views/settings/E2eAdvancedPanel.tsx
+++ b/src/components/views/settings/E2eAdvancedPanel.tsx
@@ -25,14 +25,14 @@ const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
 
 const E2eAdvancedPanel = props => {
     return <div className="mx_SettingsTab_section">
-        <span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
+        <span className="mx_SettingsTab_subheading">{ _t("Encryption") }</span>
 
         <SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS}
             level={SettingLevel.DEVICE}
         />
-        <div className="mx_E2eAdvancedPanel_settingLongDescription">{_t(
+        <div className="mx_E2eAdvancedPanel_settingLongDescription">{ _t(
             "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.",
-        )}</div>
+        ) }</div>
     </div>;
 };
 
diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx
index 73b324b739..de49c2a980 100644
--- a/src/components/views/settings/EventIndexPanel.tsx
+++ b/src/components/views/settings/EventIndexPanel.tsx
@@ -152,7 +152,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
         if (EventIndexPeg.get() !== null) {
             eventIndexingSettings = (
                 <div>
-                    <div className='mx_SettingsTab_subsectionText'>{_t(
+                    <div className='mx_SettingsTab_subsectionText'>{ _t(
                         "Securely cache encrypted messages locally for them " +
                         "to appear in search results, using %(size)s to store messages from %(rooms)s rooms.",
                         {
@@ -162,10 +162,10 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
                             count: this.state.roomCount,
                             rooms: formatCountLong(this.state.roomCount),
                         },
-                    )}</div>
+                    ) }</div>
                     <div>
                         <AccessibleButton kind="primary" onClick={this.onManage}>
-                            {_t("Manage")}
+                            { _t("Manage") }
                         </AccessibleButton>
                     </div>
                 </div>
@@ -173,16 +173,16 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
         } else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
             eventIndexingSettings = (
                 <div>
-                    <div className='mx_SettingsTab_subsectionText'>{_t(
+                    <div className='mx_SettingsTab_subsectionText'>{ _t(
                         "Securely cache encrypted messages locally for them to " +
                         "appear in search results.",
-                    )}</div>
+                    ) }</div>
                     <div>
                         <AccessibleButton kind="primary" disabled={this.state.enabling}
                             onClick={this.onEnable}>
-                            {_t("Enable")}
+                            { _t("Enable") }
                         </AccessibleButton>
-                        {this.state.enabling ? <InlineSpinner /> : <div />}
+                        { this.state.enabling ? <InlineSpinner /> : <div /> }
                     </div>
                 </div>
             );
@@ -194,7 +194,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
             );
 
             eventIndexingSettings = (
-                <div className='mx_SettingsTab_subsectionText'>{_t(
+                <div className='mx_SettingsTab_subsectionText'>{ _t(
                     "%(brand)s is missing some components required for securely " +
                     "caching encrypted messages locally. If you'd like to " +
                     "experiment with this feature, build a custom %(brand)s Desktop " +
@@ -205,13 +205,13 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
                     {
                         nativeLink: sub => <a href={nativeLink}
                             target="_blank" rel="noreferrer noopener"
-                        >{sub}</a>,
+                        >{ sub }</a>,
                     },
-                )}</div>
+                ) }</div>
             );
         } else if (!EventIndexPeg.platformHasSupport()) {
             eventIndexingSettings = (
-                <div className='mx_SettingsTab_subsectionText'>{_t(
+                <div className='mx_SettingsTab_subsectionText'>{ _t(
                     "%(brand)s can't securely cache encrypted messages locally " +
                     "while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> " +
                     "for encrypted messages to appear in search results.",
@@ -221,32 +221,32 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
                     {
                         desktopLink: sub => <a href="https://element.io/get-started"
                             target="_blank" rel="noreferrer noopener"
-                        >{sub}</a>,
+                        >{ sub }</a>,
                     },
-                )}</div>
+                ) }</div>
             );
         } else {
             eventIndexingSettings = (
                 <div className='mx_SettingsTab_subsectionText'>
                     <p>
-                        {this.state.enabling
+                        { this.state.enabling
                             ? <InlineSpinner />
                             : _t("Message search initialisation failed")
                         }
                     </p>
-                    {EventIndexPeg.error && (
+                    { EventIndexPeg.error && (
                         <details>
-                            <summary>{_t("Advanced")}</summary>
+                            <summary>{ _t("Advanced") }</summary>
                             <code>
-                                {EventIndexPeg.error.message}
+                                { EventIndexPeg.error.message }
                             </code>
                             <p>
                                 <AccessibleButton key="delete" kind="danger" onClick={this.confirmEventStoreReset}>
-                                    {_t("Reset")}
+                                    { _t("Reset") }
                                 </AccessibleButton>
                             </p>
                         </details>
-                    )}
+                    ) }
                 </div>
             );
         }
diff --git a/src/components/views/settings/IntegrationManager.js b/src/components/views/settings/IntegrationManager.js
index 2296575152..9f2985df14 100644
--- a/src/components/views/settings/IntegrationManager.js
+++ b/src/components/views/settings/IntegrationManager.js
@@ -85,7 +85,7 @@ export default class IntegrationManager extends React.Component {
             const Spinner = sdk.getComponent("elements.Spinner");
             return (
                 <div className='mx_IntegrationManager_loading'>
-                    <h3>{_t("Connecting to integration manager...")}</h3>
+                    <h3>{ _t("Connecting to integration manager...") }</h3>
                     <Spinner />
                 </div>
             );
@@ -94,8 +94,8 @@ export default class IntegrationManager extends React.Component {
         if (!this.props.connected || this.state.errored) {
             return (
                 <div className='mx_IntegrationManager_error'>
-                    <h3>{_t("Cannot connect to integration manager")}</h3>
-                    <p>{_t("The integration manager is offline or it cannot reach your homeserver.")}</p>
+                    <h3>{ _t("Cannot connect to integration manager") }</h3>
+                    <p>{ _t("The integration manager is offline or it cannot reach your homeserver.") }</p>
                 </div>
             );
         }
diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js
index e5c0e5c0b3..02eaaaeea8 100644
--- a/src/components/views/settings/ProfileSettings.js
+++ b/src/components/views/settings/ProfileSettings.js
@@ -149,12 +149,12 @@ export default class ProfileSettings extends React.Component {
         let hostingSignup = null;
         if (hostingSignupLink) {
             hostingSignup = <span className="mx_ProfileSettings_hostingSignup">
-                {_t(
+                { _t(
                     "<a>Upgrade</a> to your own domain", {},
                     {
-                        a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
+                        a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{ sub }</a>,
                     },
-                )}
+                ) }
                 <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
                     <img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
                 </a>
@@ -178,7 +178,7 @@ export default class ProfileSettings extends React.Component {
                 />
                 <div className="mx_ProfileSettings_profile">
                     <div className="mx_ProfileSettings_controls">
-                        <span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
+                        <span className="mx_SettingsTab_subheading">{ _t("Profile") }</span>
                         <Field
                             label={_t("Display Name")}
                             type="text" value={this.state.displayName}
@@ -186,8 +186,8 @@ export default class ProfileSettings extends React.Component {
                             onChange={this._onDisplayNameChanged}
                         />
                         <p>
-                            {this.state.userId}
-                            {hostingSignup}
+                            { this.state.userId }
+                            { hostingSignup }
                         </p>
                     </div>
                     <AvatarSetting
@@ -203,14 +203,14 @@ export default class ProfileSettings extends React.Component {
                         kind="link"
                         disabled={!this.state.enableProfileSave}
                     >
-                        {_t("Cancel")}
+                        { _t("Cancel") }
                     </AccessibleButton>
                     <AccessibleButton
                         onClick={this._saveProfile}
                         kind="primary"
                         disabled={!this.state.enableProfileSave}
                     >
-                        {_t("Save")}
+                        { _t("Save") }
                     </AccessibleButton>
                 </div>
             </form>
diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js
index b0292debe6..d473708ce1 100644
--- a/src/components/views/settings/SecureBackupPanel.js
+++ b/src/components/views/settings/SecureBackupPanel.js
@@ -221,7 +221,7 @@ export default class SecureBackupPanel extends React.PureComponent {
         if (error) {
             statusDescription = (
                 <div className="error">
-                    {_t("Unable to load key backup status")}
+                    { _t("Unable to load key backup status") }
                 </div>
             );
         } else if (loading) {
@@ -230,19 +230,19 @@ export default class SecureBackupPanel extends React.PureComponent {
             let restoreButtonCaption = _t("Restore from Backup");
 
             if (MatrixClientPeg.get().getKeyBackupEnabled()) {
-                statusDescription = <p>✅ {_t("This session is backing up your keys. ")}</p>;
+                statusDescription = <p>✅ { _t("This session is backing up your keys. ") }</p>;
             } else {
                 statusDescription = <>
-                    <p>{_t(
+                    <p>{ _t(
                         "This session is <b>not backing up your keys</b>, " +
                         "but you do have an existing backup you can restore from " +
                         "and add to going forward.", {},
-                        { b: sub => <b>{sub}</b> },
-                    )}</p>
-                    <p>{_t(
+                        { b: sub => <b>{ sub }</b> },
+                    ) }</p>
+                    <p>{ _t(
                         "Connect this session to key backup before signing out to avoid " +
                         "losing any keys that may only be on this session.",
-                    )}</p>
+                    ) }</p>
                 </>;
                 restoreButtonCaption = _t("Connect this session to Key Backup");
             }
@@ -253,11 +253,11 @@ export default class SecureBackupPanel extends React.PureComponent {
                 uploadStatus = "";
             } else if (sessionsRemaining > 0) {
                 uploadStatus = <div>
-                    {_t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining })} <br />
+                    { _t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining }) } <br />
                 </div>;
             } else {
                 uploadStatus = <div>
-                    {_t("All keys backed up")} <br />
+                    { _t("All keys backed up") } <br />
                 </div>;
             }
 
@@ -265,13 +265,13 @@ export default class SecureBackupPanel extends React.PureComponent {
                 const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
                 const validity = sub =>
                     <span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
-                        {sub}
+                        { sub }
                     </span>;
                 const verify = sub =>
                     <span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_SecureBackupPanel_deviceVerified' : 'mx_SecureBackupPanel_deviceNotVerified'}>
-                        {sub}
+                        { sub }
                     </span>;
-                const device = sub => <span className="mx_SecureBackupPanel_deviceName">{deviceName}</span>;
+                const device = sub => <span className="mx_SecureBackupPanel_deviceName">{ deviceName }</span>;
                 const fromThisDevice = (
                     sig.device &&
                     sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()
@@ -339,7 +339,7 @@ export default class SecureBackupPanel extends React.PureComponent {
                 }
 
                 return <div key={i}>
-                    {sigStatus}
+                    { sigStatus }
                 </div>;
             });
             if (backupSigStatus.sigs.length === 0) {
@@ -353,45 +353,45 @@ export default class SecureBackupPanel extends React.PureComponent {
 
             extraDetailsTableRows = <>
                 <tr>
-                    <td>{_t("Backup version:")}</td>
-                    <td>{backupInfo.version}</td>
+                    <td>{ _t("Backup version:") }</td>
+                    <td>{ backupInfo.version }</td>
                 </tr>
                 <tr>
-                    <td>{_t("Algorithm:")}</td>
-                    <td>{backupInfo.algorithm}</td>
+                    <td>{ _t("Algorithm:") }</td>
+                    <td>{ backupInfo.algorithm }</td>
                 </tr>
             </>;
 
             extraDetails = <>
-                {uploadStatus}
-                <div>{backupSigStatuses}</div>
-                <div>{trustedLocally}</div>
+                { uploadStatus }
+                <div>{ backupSigStatuses }</div>
+                <div>{ trustedLocally }</div>
             </>;
 
             actions.push(
                 <AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
-                    {restoreButtonCaption}
+                    { restoreButtonCaption }
                 </AccessibleButton>,
             );
 
             if (!isSecureBackupRequired()) {
                 actions.push(
                     <AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
-                        {_t("Delete Backup")}
+                        { _t("Delete Backup") }
                     </AccessibleButton>,
                 );
             }
         } else {
             statusDescription = <>
-                <p>{_t(
+                <p>{ _t(
                     "Your keys are <b>not being backed up from this session</b>.", {},
-                    { b: sub => <b>{sub}</b> },
-                )}</p>
-                <p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
+                    { b: sub => <b>{ sub }</b> },
+                ) }</p>
+                <p>{ _t("Back up your keys before signing out to avoid losing them.") }</p>
             </>;
             actions.push(
                 <AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
-                    {_t("Set up")}
+                    { _t("Set up") }
                 </AccessibleButton>,
             );
         }
@@ -399,7 +399,7 @@ export default class SecureBackupPanel extends React.PureComponent {
         if (secretStorageKeyInAccount) {
             actions.push(
                 <AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
-                    {_t("Reset")}
+                    { _t("Reset") }
                 </AccessibleButton>,
             );
         }
@@ -417,47 +417,47 @@ export default class SecureBackupPanel extends React.PureComponent {
         let actionRow;
         if (actions.length) {
             actionRow = <div className="mx_SecureBackupPanel_buttonRow">
-                {actions}
+                { actions }
             </div>;
         }
 
         return (
             <div>
-                <p>{_t(
+                <p>{ _t(
                     "Back up your encryption keys with your account data in case you " +
                     "lose access to your sessions. Your keys will be secured with a " +
                     "unique Security Key.",
-                )}</p>
-                {statusDescription}
+                ) }</p>
+                { statusDescription }
                 <details>
-                    <summary>{_t("Advanced")}</summary>
+                    <summary>{ _t("Advanced") }</summary>
                     <table className="mx_SecureBackupPanel_statusList"><tbody>
                         <tr>
-                            <td>{_t("Backup key stored:")}</td>
+                            <td>{ _t("Backup key stored:") }</td>
                             <td>{
                                 backupKeyStored === true ? _t("in secret storage") : _t("not stored")
                             }</td>
                         </tr>
                         <tr>
-                            <td>{_t("Backup key cached:")}</td>
+                            <td>{ _t("Backup key cached:") }</td>
                             <td>
-                                {backupKeyCached ? _t("cached locally") : _t("not found locally")}
-                                {backupKeyWellFormedText}
+                                { backupKeyCached ? _t("cached locally") : _t("not found locally") }
+                                { backupKeyWellFormedText }
                             </td>
                         </tr>
                         <tr>
-                            <td>{_t("Secret storage public key:")}</td>
-                            <td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
+                            <td>{ _t("Secret storage public key:") }</td>
+                            <td>{ secretStorageKeyInAccount ? _t("in account data") : _t("not found") }</td>
                         </tr>
                         <tr>
-                            <td>{_t("Secret storage:")}</td>
-                            <td>{secretStorageReady ? _t("ready") : _t("not ready")}</td>
+                            <td>{ _t("Secret storage:") }</td>
+                            <td>{ secretStorageReady ? _t("ready") : _t("not ready") }</td>
                         </tr>
-                        {extraDetailsTableRows}
+                        { extraDetailsTableRows }
                     </tbody></table>
-                    {extraDetails}
+                    { extraDetails }
                 </details>
-                {actionRow}
+                { actionRow }
             </div>
         );
     }
diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx
index dc38055c10..fd8abc0dbe 100644
--- a/src/components/views/settings/SetIdServer.tsx
+++ b/src/components/views/settings/SetIdServer.tsx
@@ -134,7 +134,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
                 { _t("Checking server") }
             </div>;
         } else if (this.state.error) {
-            return <span className='warning'>{this.state.error}</span>;
+            return <span className='warning'>{ this.state.error }</span>;
         } else {
             return null;
         }
@@ -193,8 +193,8 @@ export default class SetIdServer extends React.Component<IProps, IState> {
                             "Disconnect from the identity server <current /> and " +
                             "connect to <new /> instead?", {},
                             {
-                                current: sub => <b>{abbreviateUrl(currentClientIdServer)}</b>,
-                                new: sub => <b>{abbreviateUrl(idServer)}</b>,
+                                current: sub => <b>{ abbreviateUrl(currentClientIdServer) }</b>,
+                                new: sub => <b>{ abbreviateUrl(idServer) }</b>,
                             },
                         ),
                         button: _t("Continue"),
@@ -224,10 +224,10 @@ export default class SetIdServer extends React.Component<IProps, IState> {
             description: (
                 <div>
                     <span className="warning">
-                        {_t("The identity server you have chosen does not have any terms of service.")}
+                        { _t("The identity server you have chosen does not have any terms of service.") }
                     </span>
                     <span>
-                        &nbsp;{_t("Only continue if you trust the owner of the server.")}
+                        &nbsp;{ _t("Only continue if you trust the owner of the server.") }
                     </span>
                 </div>
             ),
@@ -243,7 +243,7 @@ export default class SetIdServer extends React.Component<IProps, IState> {
                 title: _t("Disconnect identity server"),
                 unboundMessage: _t(
                     "Disconnect from the identity server <idserver />?", {},
-                    { idserver: sub => <b>{abbreviateUrl(this.state.currentClientIdServer)}</b> },
+                    { idserver: sub => <b>{ abbreviateUrl(this.state.currentClientIdServer) }</b> },
                 ),
                 button: _t("Disconnect"),
             });
@@ -278,41 +278,41 @@ export default class SetIdServer extends React.Component<IProps, IState> {
         let message;
         let danger = false;
         const messageElements = {
-            idserver: sub => <b>{abbreviateUrl(currentClientIdServer)}</b>,
-            b: sub => <b>{sub}</b>,
+            idserver: sub => <b>{ abbreviateUrl(currentClientIdServer) }</b>,
+            b: sub => <b>{ sub }</b>,
         };
         if (!currentServerReachable) {
             message = <div>
-                <p>{_t(
+                <p>{ _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.",
                     {}, messageElements,
-                )}</p>
-                <p>{_t("You should:")}</p>
+                ) }</p>
+                <p>{ _t("You should:") }</p>
                 <ul>
-                    <li>{_t(
+                    <li>{ _t(
                         "check your browser plugins for anything that might block " +
                         "the identity server (such as Privacy Badger)",
-                    )}</li>
-                    <li>{_t("contact the administrators of identity server <idserver />", {}, {
+                    ) }</li>
+                    <li>{ _t("contact the administrators of identity server <idserver />", {}, {
                         idserver: messageElements.idserver,
-                    })}</li>
-                    <li>{_t("wait and try again later")}</li>
+                    }) }</li>
+                    <li>{ _t("wait and try again later") }</li>
                 </ul>
             </div>;
             danger = true;
             button = _t("Disconnect anyway");
         } else if (boundThreepids.length) {
             message = <div>
-                <p>{_t(
+                <p>{ _t(
                     "You are still <b>sharing your personal data</b> on the identity " +
                     "server <idserver />.", {}, messageElements,
-                )}</p>
-                <p>{_t(
+                ) }</p>
+                <p>{ _t(
                     "We recommend that you remove your email addresses and phone numbers " +
                     "from the identity server before disconnecting.",
-                )}</p>
+                ) }</p>
             </div>;
             danger = true;
             button = _t("Disconnect anyway");
@@ -361,13 +361,13 @@ export default class SetIdServer extends React.Component<IProps, IState> {
                 "You are currently using <server></server> to discover and be discoverable by " +
                 "existing contacts you know. You can change your identity server below.",
                 {},
-                { server: sub => <b>{abbreviateUrl(idServerUrl)}</b> },
+                { server: sub => <b>{ abbreviateUrl(idServerUrl) }</b> },
             );
             if (this.props.missingTerms) {
                 bodyText = _t(
                     "If you don't want to use <server /> to discover and be discoverable by existing " +
                     "contacts you know, enter another identity server below.",
-                    {}, { server: sub => <b>{abbreviateUrl(idServerUrl)}</b> },
+                    {}, { server: sub => <b>{ abbreviateUrl(idServerUrl) }</b> },
                 );
             }
         } else {
@@ -399,9 +399,9 @@ export default class SetIdServer extends React.Component<IProps, IState> {
                 discoButtonContent = <InlineSpinner />;
             }
             discoSection = <div>
-                <span className="mx_SettingsTab_subsectionText">{discoBodyText}</span>
+                <span className="mx_SettingsTab_subsectionText">{ discoBodyText }</span>
                 <AccessibleButton onClick={this.onDisconnectClicked} kind="danger_sm">
-                    {discoButtonContent}
+                    { discoButtonContent }
                 </AccessibleButton>
             </div>;
         }
@@ -409,10 +409,10 @@ export default class SetIdServer extends React.Component<IProps, IState> {
         return (
             <form className="mx_SettingsTab_section mx_SetIdServer" onSubmit={this.checkIdServer}>
                 <span className="mx_SettingsTab_subheading">
-                    {sectionTitle}
+                    { sectionTitle }
                 </span>
                 <span className="mx_SettingsTab_subsectionText">
-                    {bodyText}
+                    { bodyText }
                 </span>
                 <Field
                     label={_t("Enter a new identity server")}
@@ -429,8 +429,8 @@ export default class SetIdServer extends React.Component<IProps, IState> {
                 <AccessibleButton type="submit" kind="primary_sm"
                     onClick={this.checkIdServer}
                     disabled={!this.idServerChangeEnabled()}
-                >{_t("Change")}</AccessibleButton>
-                {discoSection}
+                >{ _t("Change") }</AccessibleButton>
+                { discoSection }
             </form>
         );
     }
diff --git a/src/components/views/settings/SetIntegrationManager.tsx b/src/components/views/settings/SetIntegrationManager.tsx
index f1922f93ee..e083efae0e 100644
--- a/src/components/views/settings/SetIntegrationManager.tsx
+++ b/src/components/views/settings/SetIntegrationManager.tsx
@@ -68,7 +68,7 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
                 "Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, " +
                 "and sticker packs.",
                 { serverName: currentManager.name },
-                { b: sub => <b>{sub}</b> },
+                { b: sub => <b>{ sub }</b> },
             );
         } else {
             bodyText = _t("Use an integration manager to manage bots, widgets, and sticker packs.");
@@ -77,18 +77,18 @@ export default class SetIntegrationManager extends React.Component<IProps, IStat
         return (
             <div className='mx_SetIntegrationManager'>
                 <div className="mx_SettingsTab_heading">
-                    <span>{_t("Manage integrations")}</span>
-                    <span className="mx_SettingsTab_subheading">{managerName}</span>
+                    <span>{ _t("Manage integrations") }</span>
+                    <span className="mx_SettingsTab_subheading">{ managerName }</span>
                     <ToggleSwitch checked={this.state.provisioningEnabled} onChange={this.onProvisioningToggled} />
                 </div>
                 <span className="mx_SettingsTab_subsectionText">
-                    {bodyText}
+                    { bodyText }
                     <br />
                     <br />
-                    {_t(
+                    { _t(
                         "Integration managers receive configuration data, and can modify widgets, " +
                         "send room invites, and set power levels on your behalf.",
-                    )}
+                    ) }
                 </span>
             </div>
         );
diff --git a/src/components/views/settings/SpellCheckSettings.tsx b/src/components/views/settings/SpellCheckSettings.tsx
index 1858412dac..c653b272c8 100644
--- a/src/components/views/settings/SpellCheckSettings.tsx
+++ b/src/components/views/settings/SpellCheckSettings.tsx
@@ -35,7 +35,7 @@ interface SpellCheckLanguagesIState {
 }
 
 export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
-    _onRemove = (e) => {
+    private onRemove = (e) => {
         e.stopPropagation();
         e.preventDefault();
 
@@ -45,9 +45,9 @@ export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellChe
     render() {
         return (
             <div className="mx_ExistingSpellCheckLanguage">
-                <span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
-                <AccessibleButton onClick={this._onRemove} kind="danger_sm">
-                    {_t("Remove")}
+                <span className="mx_ExistingSpellCheckLanguage_language">{ this.props.language }</span>
+                <AccessibleButton onClick={this.onRemove} kind="danger_sm">
+                    { _t("Remove") }
                 </AccessibleButton>
             </div>
         );
@@ -63,12 +63,12 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
         };
     }
 
-    _onRemoved = (language) => {
+    private onRemoved = (language: string) => {
         const languages = this.props.languages.filter((e) => e !== language);
         this.props.onLanguagesChange(languages);
     };
 
-    _onAddClick = (e) => {
+    private onAddClick = (e) => {
         e.stopPropagation();
         e.preventDefault();
 
@@ -81,31 +81,31 @@ export default class SpellCheckLanguages extends React.Component<SpellCheckLangu
         this.props.onLanguagesChange(this.props.languages);
     };
 
-    _onNewLanguageChange = (language: string) => {
+    private onNewLanguageChange = (language: string) => {
         if (this.state.newLanguage === language) return;
         this.setState({ newLanguage: language });
     };
 
     render() {
         const existingSpellCheckLanguages = this.props.languages.map((e) => {
-            return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
+            return <ExistingSpellCheckLanguage language={e} onRemoved={this.onRemoved} key={e} />;
         });
 
         const addButton = (
-            <AccessibleButton onClick={this._onAddClick} kind="primary">
-                {_t("Add")}
+            <AccessibleButton onClick={this.onAddClick} kind="primary">
+                { _t("Add") }
             </AccessibleButton>
         );
 
         return (
             <div className="mx_SpellCheckLanguages">
-                {existingSpellCheckLanguages}
-                <form onSubmit={this._onAddClick} noValidate={true}>
+                { existingSpellCheckLanguages }
+                <form onSubmit={this.onAddClick} noValidate={true}>
                     <SpellCheckLanguagesDropdown
                         className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
                         value={this.state.newLanguage}
-                        onOptionChange={this._onNewLanguageChange} />
-                    {addButton}
+                        onOptionChange={this.onNewLanguageChange} />
+                    { addButton }
                 </form>
             </div>
         );
diff --git a/src/components/views/settings/UpdateCheckButton.tsx b/src/components/views/settings/UpdateCheckButton.tsx
index 2781aa971d..9d88e079a7 100644
--- a/src/components/views/settings/UpdateCheckButton.tsx
+++ b/src/components/views/settings/UpdateCheckButton.tsx
@@ -42,7 +42,7 @@ function getStatusText(status: UpdateCheckStatus, errorDetail?: string) {
             return _t('Downloading update...');
         case UpdateCheckStatus.Ready:
             return _t("New version available. <a>Update now.</a>", {}, {
-                a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{sub}</AccessibleButton>,
+                a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{ sub }</AccessibleButton>,
             });
     }
 }
@@ -72,14 +72,14 @@ const UpdateCheckButton = () => {
     let suffix;
     if (state) {
         suffix = <span className="mx_UpdateCheckButton_summary">
-            {getStatusText(state.status, state.detail)}
-            {busy && <InlineSpinner />}
+            { getStatusText(state.status, state.detail) }
+            { busy && <InlineSpinner /> }
         </span>;
     }
 
     return <React.Fragment>
         <AccessibleButton onClick={onCheckForUpdateClick} kind="primary" disabled={busy}>
-            {_t("Check for update")}
+            { _t("Check for update") }
         </AccessibleButton>
         { suffix }
     </React.Fragment>;
diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js
index 3c5ba21ae5..88e2217ec1 100644
--- a/src/components/views/settings/account/EmailAddresses.js
+++ b/src/components/views/settings/account/EmailAddresses.js
@@ -88,21 +88,21 @@ export class ExistingEmailAddress extends React.Component {
             return (
                 <div className="mx_ExistingEmailAddress">
                     <span className="mx_ExistingEmailAddress_promptText">
-                        {_t("Remove %(email)s?", { email: this.props.email.address } )}
+                        { _t("Remove %(email)s?", { email: this.props.email.address } ) }
                     </span>
                     <AccessibleButton
                         onClick={this._onActuallyRemove}
                         kind="danger_sm"
                         className="mx_ExistingEmailAddress_confirmBtn"
                     >
-                        {_t("Remove")}
+                        { _t("Remove") }
                     </AccessibleButton>
                     <AccessibleButton
                         onClick={this._onDontRemove}
                         kind="link_sm"
                         className="mx_ExistingEmailAddress_confirmBtn"
                     >
-                        {_t("Cancel")}
+                        { _t("Cancel") }
                     </AccessibleButton>
                 </div>
             );
@@ -110,9 +110,9 @@ export class ExistingEmailAddress extends React.Component {
 
         return (
             <div className="mx_ExistingEmailAddress">
-                <span className="mx_ExistingEmailAddress_email">{this.props.email.address}</span>
+                <span className="mx_ExistingEmailAddress_email">{ this.props.email.address }</span>
                 <AccessibleButton onClick={this._onRemove} kind="danger_sm">
-                    {_t("Remove")}
+                    { _t("Remove") }
                 </AccessibleButton>
             </div>
         );
@@ -229,19 +229,19 @@ export default class EmailAddresses extends React.Component {
 
         let addButton = (
             <AccessibleButton onClick={this._onAddClick} kind="primary">
-                {_t("Add")}
+                { _t("Add") }
             </AccessibleButton>
         );
         if (this.state.verifying) {
             addButton = (
                 <div>
-                    <div>{_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}</div>
+                    <div>{ _t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.") }</div>
                     <AccessibleButton
                         onClick={this._onContinueClick}
                         kind="primary"
                         disabled={this.state.continueDisabled}
                     >
-                        {_t("Continue")}
+                        { _t("Continue") }
                     </AccessibleButton>
                 </div>
             );
@@ -249,7 +249,7 @@ export default class EmailAddresses extends React.Component {
 
         return (
             <div className="mx_EmailAddresses">
-                {existingEmailElements}
+                { existingEmailElements }
                 <form
                     onSubmit={this._onAddClick}
                     autoComplete="off"
@@ -264,7 +264,7 @@ export default class EmailAddresses extends React.Component {
                         value={this.state.newEmailAddress}
                         onChange={this._onChangeNewEmailAddress}
                     />
-                    {addButton}
+                    { addButton }
                 </form>
             </div>
         );
diff --git a/src/components/views/settings/account/PhoneNumbers.js b/src/components/views/settings/account/PhoneNumbers.js
index c158d323ac..604abd1bd6 100644
--- a/src/components/views/settings/account/PhoneNumbers.js
+++ b/src/components/views/settings/account/PhoneNumbers.js
@@ -83,21 +83,21 @@ export class ExistingPhoneNumber extends React.Component {
             return (
                 <div className="mx_ExistingPhoneNumber">
                     <span className="mx_ExistingPhoneNumber_promptText">
-                        {_t("Remove %(phone)s?", { phone: this.props.msisdn.address })}
+                        { _t("Remove %(phone)s?", { phone: this.props.msisdn.address }) }
                     </span>
                     <AccessibleButton
                         onClick={this._onActuallyRemove}
                         kind="danger_sm"
                         className="mx_ExistingPhoneNumber_confirmBtn"
                     >
-                        {_t("Remove")}
+                        { _t("Remove") }
                     </AccessibleButton>
                     <AccessibleButton
                         onClick={this._onDontRemove}
                         kind="link_sm"
                         className="mx_ExistingPhoneNumber_confirmBtn"
                     >
-                        {_t("Cancel")}
+                        { _t("Cancel") }
                     </AccessibleButton>
                 </div>
             );
@@ -105,9 +105,9 @@ export class ExistingPhoneNumber extends React.Component {
 
         return (
             <div className="mx_ExistingPhoneNumber">
-                <span className="mx_ExistingPhoneNumber_address">+{this.props.msisdn.address}</span>
+                <span className="mx_ExistingPhoneNumber_address">+{ this.props.msisdn.address }</span>
                 <AccessibleButton onClick={this._onRemove} kind="danger_sm">
-                    {_t("Remove")}
+                    { _t("Remove") }
                 </AccessibleButton>
             </div>
         );
@@ -230,7 +230,7 @@ export default class PhoneNumbers extends React.Component {
 
         let addVerifySection = (
             <AccessibleButton onClick={this._onAddClick} kind="primary">
-                {_t("Add")}
+                { _t("Add") }
             </AccessibleButton>
         );
         if (this.state.verifying) {
@@ -238,10 +238,10 @@ export default class PhoneNumbers extends React.Component {
             addVerifySection = (
                 <div>
                     <div>
-                        {_t("A text message has been sent to +%(msisdn)s. " +
-                            "Please enter the verification code it contains.", { msisdn: msisdn })}
+                        { _t("A text message has been sent to +%(msisdn)s. " +
+                            "Please enter the verification code it contains.", { msisdn: msisdn }) }
                         <br />
-                        {this.state.verifyError}
+                        { this.state.verifyError }
                     </div>
                     <form onSubmit={this._onContinueClick} autoComplete="off" noValidate={true}>
                         <Field
@@ -257,7 +257,7 @@ export default class PhoneNumbers extends React.Component {
                             kind="primary"
                             disabled={this.state.continueDisabled}
                         >
-                            {_t("Continue")}
+                            { _t("Continue") }
                         </AccessibleButton>
                     </form>
                 </div>
@@ -274,7 +274,7 @@ export default class PhoneNumbers extends React.Component {
 
         return (
             <div className="mx_PhoneNumbers">
-                {existingPhoneElements}
+                { existingPhoneElements }
                 <form onSubmit={this._onAddClick} autoComplete="off" noValidate={true} className="mx_PhoneNumbers_new">
                     <div className="mx_PhoneNumbers_input">
                         <Field
@@ -288,7 +288,7 @@ export default class PhoneNumbers extends React.Component {
                         />
                     </div>
                 </form>
-                {addVerifySection}
+                { addVerifySection }
             </div>
         );
     }
diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js
index 352ff1b0ba..970407774b 100644
--- a/src/components/views/settings/discovery/EmailAddresses.js
+++ b/src/components/views/settings/discovery/EmailAddresses.js
@@ -198,14 +198,14 @@ export class EmailAddress extends React.Component {
         let status;
         if (verifying) {
             status = <span>
-                {_t("Verify the link in your inbox")}
+                { _t("Verify the link in your inbox") }
                 <AccessibleButton
                     className="mx_ExistingEmailAddress_confirmBtn"
                     kind="primary_sm"
                     onClick={this.onContinueClick}
                     disabled={this.state.continueDisabled}
                 >
-                    {_t("Complete")}
+                    { _t("Complete") }
                 </AccessibleButton>
             </span>;
         } else if (bound) {
@@ -214,7 +214,7 @@ export class EmailAddress extends React.Component {
                 kind="danger_sm"
                 onClick={this.onRevokeClick}
             >
-                {_t("Revoke")}
+                { _t("Revoke") }
             </AccessibleButton>;
         } else {
             status = <AccessibleButton
@@ -222,14 +222,14 @@ export class EmailAddress extends React.Component {
                 kind="primary_sm"
                 onClick={this.onShareClick}
             >
-                {_t("Share")}
+                { _t("Share") }
             </AccessibleButton>;
         }
 
         return (
             <div className="mx_ExistingEmailAddress">
-                <span className="mx_ExistingEmailAddress_email">{address}</span>
-                {status}
+                <span className="mx_ExistingEmailAddress_email">{ address }</span>
+                { status }
             </div>
         );
     }
@@ -249,13 +249,13 @@ export default class EmailAddresses extends React.Component {
             });
         } else {
             content = <span className="mx_SettingsTab_subsectionText">
-                {_t("Discovery options will appear once you have added an email above.")}
+                { _t("Discovery options will appear once you have added an email above.") }
             </span>;
         }
 
         return (
             <div className="mx_EmailAddresses">
-                {content}
+                { content }
             </div>
         );
     }
diff --git a/src/components/views/settings/discovery/PhoneNumbers.js b/src/components/views/settings/discovery/PhoneNumbers.js
index 9df4a38f70..b6c944c733 100644
--- a/src/components/views/settings/discovery/PhoneNumbers.js
+++ b/src/components/views/settings/discovery/PhoneNumbers.js
@@ -205,9 +205,9 @@ export class PhoneNumber extends React.Component {
         if (verifying) {
             status = <span className="mx_ExistingPhoneNumber_verification">
                 <span>
-                    {_t("Please enter verification code sent via text.")}
+                    { _t("Please enter verification code sent via text.") }
                     <br />
-                    {this.state.verifyError}
+                    { this.state.verifyError }
                 </span>
                 <form onSubmit={this.onContinueClick} autoComplete="off" noValidate={true}>
                     <Field
@@ -226,7 +226,7 @@ export class PhoneNumber extends React.Component {
                 kind="danger_sm"
                 onClick={this.onRevokeClick}
             >
-                {_t("Revoke")}
+                { _t("Revoke") }
             </AccessibleButton>;
         } else {
             status = <AccessibleButton
@@ -234,14 +234,14 @@ export class PhoneNumber extends React.Component {
                 kind="primary_sm"
                 onClick={this.onShareClick}
             >
-                {_t("Share")}
+                { _t("Share") }
             </AccessibleButton>;
         }
 
         return (
             <div className="mx_ExistingPhoneNumber">
-                <span className="mx_ExistingPhoneNumber_address">+{address}</span>
-                {status}
+                <span className="mx_ExistingPhoneNumber_address">+{ address }</span>
+                { status }
             </div>
         );
     }
@@ -261,13 +261,13 @@ export default class PhoneNumbers extends React.Component {
             });
         } else {
             content = <span className="mx_SettingsTab_subsectionText">
-                {_t("Discovery options will appear once you have added a phone number above.")}
+                { _t("Discovery options will appear once you have added a phone number above.") }
             </span>;
         }
 
         return (
             <div className="mx_PhoneNumbers">
-                {content}
+                { content }
             </div>
         );
     }
diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
index eda3419d14..9322eab711 100644
--- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
@@ -116,8 +116,8 @@ export default class AdvancedRoomSettingsTab extends React.Component<IProps, ISt
                             "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": (sub) => <b>{sub}</b>,
-                                "i": (sub) => <i>{sub}</i>,
+                                "b": (sub) => <b>{ sub }</b>,
+                                "i": (sub) => <i>{ sub }</i>,
                             },
                         ) }
                     </p>
diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
index fb144da399..c8188250b1 100644
--- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
@@ -61,36 +61,36 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
         let content: JSX.Element;
         if (bridgeEvents.length > 0) {
             content = <div>
-                <p>{_t(
+                <p>{ _t(
                     "This room is bridging messages to the following platforms. " +
                     "<a>Learn more.</a>", {},
                     {
                         // TODO: We don't have this link yet: this will prevent the translators
                         // having to re-translate the string when we do.
-                        a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
+                        a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{ sub }</a>,
                     },
-                )}</p>
+                ) }</p>
                 <ul className="mx_RoomSettingsDialog_BridgeList">
                     { bridgeEvents.map((event) => this.renderBridgeCard(event, room)) }
                 </ul>
             </div>;
         } else {
-            content = <p>{_t(
+            content = <p>{ _t(
                 "This room isn’t bridging messages to any platforms. " +
                 "<a>Learn more.</a>", {},
                 {
                     // TODO: We don't have this link yet: this will prevent the translators
                     // having to re-translate the string when we do.
-                    a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
+                    a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{ sub }</a>,
                 },
-            )}</p>;
+            ) }</p>;
         }
 
         return (
             <div className="mx_SettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Bridges")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Bridges") }</div>
                 <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
-                    {content}
+                    { content }
                 </div>
             </div>
         );
diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
index 125558732d..e2f30192b9 100644
--- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
@@ -65,7 +65,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
         const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
 
         let urlPreviewSettings = <>
-            <span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
+            <span className='mx_SettingsTab_subheading'>{ _t("URL Previews") }</span>
             <div className='mx_SettingsTab_section'>
                 <UrlPreviewSettings room={room} />
             </div>
@@ -77,7 +77,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
         let flairSection;
         if (SettingsStore.getValue(UIFeature.Flair)) {
             flairSection = <>
-                <span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t("Flair") }</span>
                 <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
                     <RelatedGroupSettings
                         roomId={room.roomId}
@@ -90,22 +90,22 @@ export default class GeneralRoomSettingsTab extends React.Component {
 
         return (
             <div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("General")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("General") }</div>
                 <div className='mx_SettingsTab_section mx_GeneralRoomSettingsTab_profileSection'>
                     <RoomProfileSettings roomId={this.props.roomId} />
                 </div>
 
-                <div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Room Addresses") }</div>
                 <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
                     <AliasSettings roomId={this.props.roomId}
                         canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
                         canonicalAliasEvent={canonicalAliasEv} />
                 </div>
-                <div className="mx_SettingsTab_heading">{_t("Other")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Other") }</div>
                 { flairSection }
                 { urlPreviewSettings }
 
-                <span className='mx_SettingsTab_subheading'>{_t("Leave room")}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t("Leave room") }</span>
                 <div className='mx_SettingsTab_section'>
                     <AccessibleButton kind='danger' onClick={this._onLeaveClick}>
                         { _t('Leave room') }
diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js
index cb65e13825..9200fb65d1 100644
--- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js
+++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js
@@ -142,36 +142,36 @@ export default class NotificationsSettingsTab extends React.Component {
         if (this.state.uploadedFile) {
             currentUploadedFile = (
                 <div>
-                    <span>{_t("Uploaded sound")}: <code>{this.state.uploadedFile.name}</code></span>
+                    <span>{ _t("Uploaded sound") }: <code>{ this.state.uploadedFile.name }</code></span>
                 </div>
             );
         }
 
         return (
             <div className="mx_SettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
                 <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
-                    <span className='mx_SettingsTab_subheading'>{_t("Sounds")}</span>
+                    <span className='mx_SettingsTab_subheading'>{ _t("Sounds") }</span>
                     <div>
-                        <span>{_t("Notification sound")}: <code>{this.state.currentSound}</code></span><br />
+                        <span>{ _t("Notification sound") }: <code>{ this.state.currentSound }</code></span><br />
                         <AccessibleButton className="mx_NotificationSound_resetSound" disabled={this.state.currentSound == "default"} onClick={this._clearSound.bind(this)} kind="primary">
-                            {_t("Reset")}
+                            { _t("Reset") }
                         </AccessibleButton>
                     </div>
                     <div>
-                        <h3>{_t("Set a new custom sound")}</h3>
+                        <h3>{ _t("Set a new custom sound") }</h3>
                         <form autoComplete="off" noValidate={true}>
                             <input ref={this._soundUpload} className="mx_NotificationSound_soundUpload" type="file" onChange={this._onSoundUploadChanged.bind(this)} accept="audio/*" />
                         </form>
 
-                        {currentUploadedFile}
+                        { currentUploadedFile }
 
                         <AccessibleButton className="mx_NotificationSound_browse" onClick={this._triggerUploader.bind(this)} kind="primary">
-                            {_t("Browse")}
+                            { _t("Browse") }
                         </AccessibleButton>
 
                         <AccessibleButton className="mx_NotificationSound_save" disabled={this.state.uploadedFile == null} onClick={this._onClickSaveSound.bind(this)} kind="primary">
-                            {_t("Save")}
+                            { _t("Save") }
                         </AccessibleButton>
                         <br />
                     </div>
diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
index 2679dcaa57..edc0220921 100644
--- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
@@ -102,10 +102,10 @@ export class BannedUser extends React.Component<IBannedUserProps> {
         const userId = this.props.member.name === this.props.member.userId ? null : this.props.member.userId;
         return (
             <li>
-                {unbanButton}
+                { unbanButton }
                 <span title={_t("Banned by %(displayName)s", { displayName: this.props.by })}>
-                    <strong>{ this.props.member.name }</strong> {userId}
-                    {this.props.reason ? " " + _t('Reason') + ": " + this.props.reason : ""}
+                    <strong>{ this.props.member.name }</strong> { userId }
+                    { this.props.reason ? " " + _t('Reason') + ": " + this.props.reason : "" }
                 </span>
             </li>
         );
@@ -273,7 +273,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
             parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue),
         );
 
-        let privilegedUsersSection = <div>{_t('No users have specific privileges in this room')}</div>;
+        let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }</div>;
         let mutedUsersSection;
         if (Object.keys(userLevels).length) {
             const privilegedUsers = [];
@@ -320,14 +320,14 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
                 privilegedUsersSection =
                     <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
                         <div className='mx_SettingsTab_subheading'>{ _t('Privileged Users') }</div>
-                        {privilegedUsers}
+                        { privilegedUsers }
                     </div>;
             }
             if (mutedUsers.length) {
                 mutedUsersSection =
                     <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
                         <div className='mx_SettingsTab_subheading'>{ _t('Muted Users') }</div>
-                        {mutedUsers}
+                        { mutedUsers }
                     </div>;
             }
         }
@@ -340,7 +340,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
                 <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
                     <div className='mx_SettingsTab_subheading'>{ _t('Banned users') }</div>
                     <ul>
-                        {banned.map((member) => {
+                        { banned.map((member) => {
                             const banEvent = member.events.member.getContent();
                             const sender = room.getMember(member.events.member.getSender());
                             let bannedBy = member.events.member.getSender(); // start by falling back to mxid
@@ -351,7 +351,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
                                     by={bannedBy}
                                 />
                             );
-                        })}
+                        }) }
                     </ul>
                 </div>;
         }
@@ -409,15 +409,15 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
 
         return (
             <div className="mx_SettingsTab mx_RolesRoomSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Roles & Permissions")}</div>
-                {privilegedUsersSection}
-                {mutedUsersSection}
-                {bannedUsersSection}
+                <div className="mx_SettingsTab_heading">{ _t("Roles & Permissions") }</div>
+                { privilegedUsersSection }
+                { mutedUsersSection }
+                { bannedUsersSection }
                 <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
-                    <span className='mx_SettingsTab_subheading'>{_t("Permissions")}</span>
-                    <p>{_t('Select the roles required to change various parts of the room')}</p>
-                    {powerSelectors}
-                    {eventPowerSelectors}
+                    <span className='mx_SettingsTab_subheading'>{ _t("Permissions") }</span>
+                    <p>{ _t('Select the roles required to change various parts of the room') }</p>
+                    { powerSelectors }
+                    { eventPowerSelectors }
                 </div>
             </div>
         );
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
index 78d8fecf3b..88bc2046ce 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
@@ -78,7 +78,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
     }
 
     // TODO: [REACT-WARNING] Move this to constructor
-    async UNSAFE_componentWillMount() { // eslint-disable-line camelcase
+    async UNSAFE_componentWillMount() { // eslint-disable-line
         MatrixClientPeg.get().on("RoomState.events", this.onStateEvent);
 
         const room = MatrixClientPeg.get().getRoom(this.props.roomId);
@@ -135,7 +135,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
                 {
                     a: sub => <a href="https://element.io/help#encryption"
                         rel="noreferrer noopener" target="_blank"
-                    >{sub}</a>,
+                    >{ sub }</a>,
                 },
             ),
             onFinished: (confirm) => {
@@ -289,8 +289,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
                 <div className='mx_SecurityRoomSettingsTab_warning'>
                     <img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
                     <span>
-                        {_t("Guests cannot join this room even if explicitly invited.")}&nbsp;
-                        <a href="" onClick={this.fixGuestAccess}>{_t("Click here to fix")}</a>
+                        { _t("Guests cannot join this room even if explicitly invited.") }&nbsp;
+                        <a href="" onClick={this.fixGuestAccess}>{ _t("Click here to fix") }</a>
                     </span>
                 </div>
             );
@@ -302,7 +302,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
                 <div className='mx_SecurityRoomSettingsTab_warning'>
                     <img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
                     <span>
-                        {_t("To link to this room, please add an address.")}
+                        { _t("To link to this room, please add an address.") }
                     </span>
                 </div>
             );
@@ -310,8 +310,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
 
         return (
             <div>
-                {guestWarning}
-                {aliasWarning}
+                { guestWarning }
+                { aliasWarning }
                 <StyledRadioGroup
                     name="roomVis"
                     value={joinRule}
@@ -373,8 +373,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
         return (
             <div>
                 <div>
-                    {_t('Changes to who can read history will only apply to future messages in this room. ' +
-                        'The visibility of existing history will be unchanged.')}
+                    { _t('Changes to who can read history will only apply to future messages in this room. ' +
+                        'The visibility of existing history will be unchanged.') }
                 </div>
                 <StyledRadioGroup
                     name="historyVis"
@@ -405,9 +405,9 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
         }
 
         let historySection = (<>
-            <span className='mx_SettingsTab_subheading'>{_t("Who can read history?")}</span>
+            <span className='mx_SettingsTab_subheading'>{ _t("Who can read history?") }</span>
             <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
-                {this.renderHistory()}
+                { this.renderHistory() }
             </div>
         </>);
         if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) {
@@ -416,27 +416,27 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
 
         return (
             <div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Security & Privacy") }</div>
 
-                <span className='mx_SettingsTab_subheading'>{_t("Encryption")}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t("Encryption") }</span>
                 <div className='mx_SettingsTab_section mx_SecurityRoomSettingsTab_encryptionSection'>
                     <div>
                         <div className='mx_SettingsTab_subsectionText'>
-                            <span>{_t("Once enabled, encryption cannot be disabled.")}</span>
+                            <span>{ _t("Once enabled, encryption cannot be disabled.") }</span>
                         </div>
                         <LabelledToggleSwitch value={isEncrypted} onChange={this.onEncryptionChange}
                             label={_t("Encrypted")} disabled={!canEnableEncryption}
                         />
                     </div>
-                    {encryptionSettings}
+                    { encryptionSettings }
                 </div>
 
-                <span className='mx_SettingsTab_subheading'>{_t("Who can access this room?")}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t("Who can access this room?") }</span>
                 <div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
-                    {this.renderRoomAccess()}
+                    { this.renderRoomAccess() }
                 </div>
 
-                {historySection}
+                { historySection }
             </div>
         );
     }
diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
index a94821e94a..bd488f42b6 100644
--- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
@@ -37,6 +37,8 @@ import StyledRadioGroup from "../../../elements/StyledRadioGroup";
 import { SettingLevel } from "../../../../../settings/SettingLevel";
 import { UIFeature } from "../../../../../settings/UIFeature";
 import { Layout } from "../../../../../settings/Layout";
+import classNames from 'classnames';
+import StyledRadioButton from '../../../elements/StyledRadioButton';
 import { replaceableComponent } from "../../../../../utils/replaceableComponent";
 import { compare } from "../../../../../utils/strings";
 
@@ -241,6 +243,19 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
         this.setState({ customThemeUrl: e.target.value });
     };
 
+    private onLayoutChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
+        let layout;
+        switch (e.target.value) {
+            case "irc": layout = Layout.IRC; break;
+            case "group": layout = Layout.Group; break;
+            case "bubble": layout = Layout.Bubble; break;
+        }
+
+        this.setState({ layout: layout });
+
+        SettingsStore.setValue("layout", null, SettingLevel.DEVICE, layout);
+    };
+
     private onIRCLayoutChange = (enabled: boolean) => {
         if (enabled) {
             this.setState({ layout: Layout.IRC });
@@ -260,7 +275,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
                     checked={this.state.useSystemTheme}
                     onChange={(e) => this.onUseSystemThemeChanged(e.target.checked)}
                 >
-                    {SettingsStore.getDisplayName("use_system_theme")}
+                    { SettingsStore.getDisplayName("use_system_theme") }
                 </StyledCheckbox>
             </div>;
         }
@@ -270,9 +285,9 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
             let messageElement = null;
             if (this.state.customThemeMessage.text) {
                 if (this.state.customThemeMessage.isError) {
-                    messageElement = <div className='text-error'>{this.state.customThemeMessage.text}</div>;
+                    messageElement = <div className='text-error'>{ this.state.customThemeMessage.text }</div>;
                 } else {
-                    messageElement = <div className='text-success'>{this.state.customThemeMessage.text}</div>;
+                    messageElement = <div className='text-success'>{ this.state.customThemeMessage.text }</div>;
                 }
             }
             customThemeForm = (
@@ -290,8 +305,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
                             onClick={this.onAddCustomTheme}
                             type="submit" kind="primary_sm"
                             disabled={!this.state.customThemeUrl.trim()}
-                        >{_t("Add theme")}</AccessibleButton>
-                        {messageElement}
+                        >{ _t("Add theme") }</AccessibleButton>
+                        { messageElement }
                     </form>
                 </div>
             );
@@ -306,8 +321,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
         const orderedThemes = [...builtInThemes, ...customThemes];
         return (
             <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_themeSection">
-                <span className="mx_SettingsTab_subheading">{_t("Theme")}</span>
-                {systemThemeSection}
+                <span className="mx_SettingsTab_subheading">{ _t("Theme") }</span>
+                { systemThemeSection }
                 <div className="mx_ThemeSelectors">
                     <StyledRadioGroup
                         name="theme"
@@ -322,7 +337,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
                         outlined
                     />
                 </div>
-                {customThemeForm}
+                { customThemeForm }
             </div>
         );
     }
@@ -330,7 +345,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
     private renderFontSection() {
         return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_fontScaling">
 
-            <span className="mx_SettingsTab_subheading">{_t("Font size")}</span>
+            <span className="mx_SettingsTab_subheading">{ _t("Font size") }</span>
             <EventTilePreview
                 className="mx_AppearanceUserSettingsTab_fontSlider_preview"
                 message={this.MESSAGE_PREVIEW_TEXT}
@@ -373,6 +388,77 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
         </div>;
     }
 
+    private renderLayoutSection = () => {
+        return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Layout">
+            <span className="mx_SettingsTab_subheading">{ _t("Message layout") }</span>
+
+            <div className="mx_AppearanceUserSettingsTab_Layout_RadioButtons">
+                <div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
+                    mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.IRC,
+                })}>
+                    <EventTilePreview
+                        className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
+                        message={this.MESSAGE_PREVIEW_TEXT}
+                        layout={Layout.IRC}
+                        userId={this.state.userId}
+                        displayName={this.state.displayName}
+                        avatarUrl={this.state.avatarUrl}
+                    />
+                    <StyledRadioButton
+                        name="layout"
+                        value="irc"
+                        checked={this.state.layout === Layout.IRC}
+                        onChange={this.onLayoutChange}
+                    >
+                        { _t("IRC") }
+                    </StyledRadioButton>
+                </div>
+                <div className="mx_AppearanceUserSettingsTab_spacer" />
+                <div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
+                    mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout == Layout.Group,
+                })}>
+                    <EventTilePreview
+                        className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
+                        message={this.MESSAGE_PREVIEW_TEXT}
+                        layout={Layout.Group}
+                        userId={this.state.userId}
+                        displayName={this.state.displayName}
+                        avatarUrl={this.state.avatarUrl}
+                    />
+                    <StyledRadioButton
+                        name="layout"
+                        value="group"
+                        checked={this.state.layout == Layout.Group}
+                        onChange={this.onLayoutChange}
+                    >
+                        { _t("Modern") }
+                    </StyledRadioButton>
+                </div>
+                <div className="mx_AppearanceUserSettingsTab_spacer" />
+                <div className={classNames("mx_AppearanceUserSettingsTab_Layout_RadioButton", {
+                    mx_AppearanceUserSettingsTab_Layout_RadioButton_selected: this.state.layout === Layout.Bubble,
+                })}>
+                    <EventTilePreview
+                        className="mx_AppearanceUserSettingsTab_Layout_RadioButton_preview"
+                        message={this.MESSAGE_PREVIEW_TEXT}
+                        layout={Layout.Bubble}
+                        userId={this.state.userId}
+                        displayName={this.state.displayName}
+                        avatarUrl={this.state.avatarUrl}
+                    />
+                    <StyledRadioButton
+                        name="layout"
+                        value="bubble"
+                        checked={this.state.layout == Layout.Bubble}
+                        onChange={this.onLayoutChange}
+                    >
+                        { _t("Message bubbles") }
+                    </StyledRadioButton>
+                </div>
+            </div>
+        </div>;
+    };
+
     private renderAdvancedSection() {
         if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
 
@@ -381,7 +467,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
             className="mx_AppearanceUserSettingsTab_AdvancedToggle"
             onClick={() => this.setState({ showAdvanced: !this.state.showAdvanced })}
         >
-            {this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced")}
+            { this.state.showAdvanced ? _t("Hide advanced") : _t("Show advanced") }
         </div>;
 
         let advanced: React.ReactNode;
@@ -396,14 +482,17 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
                     name="useCompactLayout"
                     level={SettingLevel.DEVICE}
                     useCheckbox={true}
-                    disabled={this.state.layout == Layout.IRC}
+                    disabled={this.state.layout !== Layout.Group}
                 />
-                <StyledCheckbox
-                    checked={this.state.layout == Layout.IRC}
-                    onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
-                >
-                    {_t("Enable experimental, compact IRC style layout")}
-                </StyledCheckbox>
+
+                { !SettingsStore.getValue("feature_new_layout_switcher") ?
+                    <StyledCheckbox
+                        checked={this.state.layout == Layout.IRC}
+                        onChange={(ev) => this.onIRCLayoutChange(ev.target.checked)}
+                    >
+                        { _t("Enable experimental, compact IRC style layout") }
+                    </StyledCheckbox> : null
+                }
 
                 <SettingsFlag
                     name="useSystemFont"
@@ -429,8 +518,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
             </>;
         }
         return <div className="mx_SettingsTab_section mx_AppearanceUserSettingsTab_Advanced">
-            {toggle}
-            {advanced}
+            { toggle }
+            { advanced }
         </div>;
     }
 
@@ -439,13 +528,14 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
 
         return (
             <div className="mx_SettingsTab mx_AppearanceUserSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Customise your appearance")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Customise your appearance") }</div>
                 <div className="mx_SettingsTab_SubHeading">
-                    {_t("Appearance Settings only affect this %(brand)s session.", { brand })}
+                    { _t("Appearance Settings only affect this %(brand)s session.", { brand }) }
                 </div>
-                {this.renderThemeSection()}
-                {this.renderFontSection()}
-                {this.renderAdvancedSection()}
+                { this.renderThemeSection() }
+                { SettingsStore.getValue("feature_new_layout_switcher") ? this.renderLayoutSection() : null }
+                { this.renderFontSection() }
+                { this.renderAdvancedSection() }
             </div>
         );
     }
diff --git a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
index 272f5ec071..180cb5df2c 100644
--- a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
@@ -24,7 +24,7 @@ export default class FlairUserSettingsTab extends React.Component {
     render() {
         return (
             <div className="mx_SettingsTab">
-                <span className="mx_SettingsTab_heading">{_t("Flair")}</span>
+                <span className="mx_SettingsTab_heading">{ _t("Flair") }</span>
                 <div className="mx_SettingsTab_section">
                     <GroupUserSettings />
                 </div>
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index f1b7df3eb5..2a6e8937a3 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -289,11 +289,11 @@ export default class GeneralUserSettingsTab extends React.Component {
                     onMsisdnsChange={this._onMsisdnsChange}
                 />;
             threepidSection = <div>
-                <span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
-                {emails}
+                <span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
+                { emails }
 
-                <span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
-                {msisdns}
+                <span className="mx_SettingsTab_subheading">{ _t("Phone numbers") }</span>
+                { msisdns }
             </div>;
         } else if (this.state.serverSupportsSeparateAddAndBind === null) {
             threepidSection = <Spinner />;
@@ -308,12 +308,12 @@ export default class GeneralUserSettingsTab extends React.Component {
 
         return (
             <div className="mx_SettingsTab_section mx_GeneralUserSettingsTab_accountSection">
-                <span className="mx_SettingsTab_subheading">{_t("Account")}</span>
+                <span className="mx_SettingsTab_subheading">{ _t("Account") }</span>
                 <p className="mx_SettingsTab_subsectionText">
-                    {passwordChangeText}
+                    { passwordChangeText }
                 </p>
-                {passwordChangeForm}
-                {threepidSection}
+                { passwordChangeForm }
+                { threepidSection }
             </div>
         );
     }
@@ -322,7 +322,7 @@ export default class GeneralUserSettingsTab extends React.Component {
         // TODO: Convert to new-styled Field
         return (
             <div className="mx_SettingsTab_section">
-                <span className="mx_SettingsTab_subheading">{_t("Language and region")}</span>
+                <span className="mx_SettingsTab_subheading">{ _t("Language and region") }</span>
                 <LanguageDropdown
                     className="mx_GeneralUserSettingsTab_languageInput"
                     onOptionChange={this._onLanguageChange}
@@ -335,7 +335,7 @@ export default class GeneralUserSettingsTab extends React.Component {
     _renderSpellCheckSection() {
         return (
             <div className="mx_SettingsTab_section">
-                <span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
+                <span className="mx_SettingsTab_subheading">{ _t("Spell check dictionaries") }</span>
                 <SpellCheckSettings
                     languages={this.state.spellCheckLanguages}
                     onLanguagesChange={this._onSpellCheckLanguagesChange}
@@ -350,11 +350,11 @@ export default class GeneralUserSettingsTab extends React.Component {
         if (this.state.requiredPolicyInfo.hasTerms) {
             const InlineTermsAgreement = sdk.getComponent("views.terms.InlineTermsAgreement");
             const intro = <span className="mx_SettingsTab_subsectionText">
-                {_t(
+                { _t(
                     "Agree to the identity server (%(serverName)s) Terms of Service to " +
                     "allow yourself to be discoverable by email address or phone number.",
                     { serverName: this.state.idServerName },
-                )}
+                ) }
             </span>;
             return (
                 <div>
@@ -377,16 +377,16 @@ export default class GeneralUserSettingsTab extends React.Component {
         const msisdns = this.state.loading3pids ? <Spinner /> : <PhoneNumbers msisdns={this.state.msisdns} />;
 
         const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
-            <span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
-            {emails}
+            <span className="mx_SettingsTab_subheading">{ _t("Email addresses") }</span>
+            { emails }
 
-            <span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
-            {msisdns}
+            <span className="mx_SettingsTab_subheading">{ _t("Phone numbers") }</span>
+            { msisdns }
         </div> : null;
 
         return (
             <div className="mx_SettingsTab_section">
-                {threepidSection}
+                { threepidSection }
                 { /* has its own heading as it includes the current identity server */ }
                 <SetIdServer />
             </div>
@@ -397,12 +397,12 @@ export default class GeneralUserSettingsTab extends React.Component {
         // TODO: Improve warning text for account deactivation
         return (
             <div className="mx_SettingsTab_section">
-                <span className="mx_SettingsTab_subheading">{_t("Account management")}</span>
+                <span className="mx_SettingsTab_subheading">{ _t("Account management") }</span>
                 <span className="mx_SettingsTab_subsectionText">
-                    {_t("Deactivating your account is a permanent action - be careful!")}
+                    { _t("Deactivating your account is a permanent action - be careful!") }
                 </span>
                 <AccessibleButton onClick={this._onDeactivateClicked} kind="danger">
-                    {_t("Deactivate Account")}
+                    { _t("Deactivate Account") }
                 </AccessibleButton>
             </div>
         );
@@ -434,28 +434,28 @@ export default class GeneralUserSettingsTab extends React.Component {
         let accountManagementSection;
         if (SettingsStore.getValue(UIFeature.Deactivate)) {
             accountManagementSection = <>
-                <div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
-                {this._renderManagementSection()}
+                <div className="mx_SettingsTab_heading">{ _t("Deactivate account") }</div>
+                { this._renderManagementSection() }
             </>;
         }
 
         let discoverySection;
         if (SettingsStore.getValue(UIFeature.IdentityServer)) {
             discoverySection = <>
-                <div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
-                {this._renderDiscoverySection()}
+                <div className="mx_SettingsTab_heading">{ discoWarning } { _t("Discovery") }</div>
+                { this._renderDiscoverySection() }
             </>;
         }
 
         return (
             <div className="mx_SettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("General")}</div>
-                {this._renderProfileSection()}
-                {this._renderAccountSection()}
-                {this._renderLanguageSection()}
-                {supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null}
+                <div className="mx_SettingsTab_heading">{ _t("General") }</div>
+                { this._renderProfileSection() }
+                { this._renderAccountSection() }
+                { this._renderLanguageSection() }
+                { supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null }
                 { discoverySection }
-                {this._renderIntegrationManagerSection() /* Has its own title */}
+                { this._renderIntegrationManagerSection() /* Has its own title */ }
                 { accountManagementSection }
             </div>
         );
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
index f2857720a5..33de634611 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
@@ -112,15 +112,15 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
         const legalLinks = [];
         for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
             legalLinks.push(<div key={tocEntry.url}>
-                <a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{tocEntry.text}</a>
+                <a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{ tocEntry.text }</a>
             </div>);
         }
 
         return (
             <div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
-                <span className='mx_SettingsTab_subheading'>{_t("Legal")}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t("Legal") }</span>
                 <div className='mx_SettingsTab_subsectionText'>
-                    {legalLinks}
+                    { legalLinks }
                 </div>
             </div>
         );
@@ -131,7 +131,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
         // Also, &nbsp; is ugly but necessary.
         return (
             <div className='mx_SettingsTab_section'>
-                <span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t("Credits") }</span>
                 <ul>
                     <li>
                         The <a href="themes/element/img/backgrounds/lake.jpg" rel="noreferrer noopener"
@@ -189,14 +189,14 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
                     rel="noreferrer noopener"
                     target="_blank"
                 >
-                    {sub}
+                    { sub }
                 </a>,
             },
         );
         if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
             faqText = (
                 <div>
-                    {_t(
+                    { _t(
                         'For help with using %(brand)s, click <a>here</a> or start a chat with our ' +
                         'bot using the button below.',
                         {
@@ -208,13 +208,13 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
                                 rel='noreferrer noopener'
                                 target='_blank'
                             >
-                                {sub}
+                                { sub }
                             </a>,
                         },
-                    )}
+                    ) }
                     <div>
                         <AccessibleButton onClick={this.onStartBotChat} kind='primary'>
-                            {_t("Chat with %(brand)s Bot", { brand })}
+                            { _t("Chat with %(brand)s Bot", { brand }) }
                         </AccessibleButton>
                     </div>
                 </div>
@@ -235,29 +235,29 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
         if (SdkConfig.get().bug_report_endpoint_url) {
             bugReportingSection = (
                 <div className="mx_SettingsTab_section">
-                    <span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
+                    <span className='mx_SettingsTab_subheading'>{ _t('Bug reporting') }</span>
                     <div className='mx_SettingsTab_subsectionText'>
-                        {_t(
+                        { _t(
                             "If you've submitted a bug via GitHub, debug logs can help " +
                             "us track down the problem. Debug logs contain application " +
                             "usage data including your username, the IDs or aliases of " +
                             "the rooms or groups you have visited and the usernames of " +
                             "other users. They do not contain messages.",
-                        )}
+                        ) }
                         <div className='mx_HelpUserSettingsTab_debugButton'>
                             <AccessibleButton onClick={this.onBugReport} kind='primary'>
-                                {_t("Submit debug logs")}
+                                { _t("Submit debug logs") }
                             </AccessibleButton>
                         </div>
-                        {_t(
+                        { _t(
                             "To report a Matrix-related security issue, please read the Matrix.org " +
                             "<a>Security Disclosure Policy</a>.", {},
                             {
                                 a: sub => <a href="https://matrix.org/security-disclosure-policy/"
                                     rel="noreferrer noopener" target="_blank"
-                                >{sub}</a>,
+                                >{ sub }</a>,
                             },
-                        )}
+                        ) }
                     </div>
                 </div>
             );
@@ -265,39 +265,39 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
 
         return (
             <div className="mx_SettingsTab mx_HelpUserSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Help & About") }</div>
                 { bugReportingSection }
                 <div className='mx_SettingsTab_section'>
-                    <span className='mx_SettingsTab_subheading'>{_t("FAQ")}</span>
+                    <span className='mx_SettingsTab_subheading'>{ _t("FAQ") }</span>
                     <div className='mx_SettingsTab_subsectionText'>
-                        {faqText}
+                        { faqText }
                     </div>
                     <AccessibleButton kind="primary" onClick={KeyboardShortcuts.toggleDialog}>
                         { _t("Keyboard Shortcuts") }
                     </AccessibleButton>
                 </div>
                 <div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
-                    <span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
+                    <span className='mx_SettingsTab_subheading'>{ _t("Versions") }</span>
                     <div className='mx_SettingsTab_subsectionText'>
-                        {_t("%(brand)s version:", { brand })} {appVersion}<br />
-                        {_t("olm version:")} {olmVersion}<br />
-                        {updateButton}
+                        { _t("%(brand)s version:", { brand }) } { appVersion }<br />
+                        { _t("olm version:") } { olmVersion }<br />
+                        { updateButton }
                     </div>
                 </div>
-                {this.renderLegal()}
-                {this.renderCredits()}
+                { this.renderLegal() }
+                { this.renderCredits() }
                 <div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
-                    <span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
+                    <span className='mx_SettingsTab_subheading'>{ _t("Advanced") }</span>
                     <div className='mx_SettingsTab_subsectionText'>
-                        {_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
-                        {_t("Identity server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
+                        { _t("Homeserver is") } <code>{ MatrixClientPeg.get().getHomeserverUrl() }</code><br />
+                        { _t("Identity server is") } <code>{ MatrixClientPeg.get().getIdentityServerUrl() }</code><br />
                         <br />
                         <details>
-                            <summary>{_t("Access Token")}</summary><br />
-                            <b>{_t("Your access token gives full access to your account."
-                               + " Do not share it with anyone." )}</b>
+                            <summary>{ _t("Access Token") }</summary><br />
+                            <b>{ _t("Your access token gives full access to your account."
+                               + " Do not share it with anyone." ) }</b>
                             <div className="mx_HelpUserSettingsTab_accessToken">
-                                <code>{MatrixClientPeg.get().getAccessToken()}</code>
+                                <code>{ MatrixClientPeg.get().getAccessToken() }</code>
                                 <AccessibleTooltipButton
                                     title={_t("Copy")}
                                     onClick={this.onAccessTokenCopyClick}
@@ -307,7 +307,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
                         </details><br />
                         <div className='mx_HelpUserSettingsTab_debugButton'>
                             <AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
-                                {_t("Clear cache and reload")}
+                                { _t("Clear cache and reload") }
                             </AccessibleButton>
                         </div>
                     </div>
diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
index abf9709f50..aace4ca557 100644
--- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js
@@ -69,7 +69,7 @@ export default class LabsUserSettingsTab extends React.Component {
             const flags = labs.map(f => <LabsSettingToggle featureId={f} key={f} />);
 
             labsSection = <div className="mx_SettingsTab_section">
-                {flags}
+                { flags }
                 <SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
                 <SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
                 <SettingsFlag name="lowBandwidth" level={SettingLevel.DEVICE} />
@@ -79,7 +79,7 @@ export default class LabsUserSettingsTab extends React.Component {
 
         return (
             <div className="mx_SettingsTab mx_LabsUserSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Labs")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Labs") }</div>
                 <div className='mx_SettingsTab_subsectionText'>
                     {
                         _t('Feeling experimental? Labs are the best way to get things early, ' +
@@ -87,7 +87,7 @@ export default class LabsUserSettingsTab extends React.Component {
                             '<a>Learn more</a>.', {}, {
                             'a': (sub) => {
                                 return <a href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
-                                    rel='noreferrer noopener' target='_blank'>{sub}</a>;
+                                    rel='noreferrer noopener' target='_blank'>{ sub }</a>;
                             },
                         })
                     }
diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx
index 41c44e65a0..0653198aa0 100644
--- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx
@@ -140,23 +140,23 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
         const name = room ? room.name : list.roomId;
 
         const renderRules = (rules: ListRule[]) => {
-            if (rules.length === 0) return <i>{_t("None")}</i>;
+            if (rules.length === 0) return <i>{ _t("None") }</i>;
 
             const tiles = [];
             for (const rule of rules) {
-                tiles.push(<li key={rule.kind + rule.entity}><code>{rule.entity}</code></li>);
+                tiles.push(<li key={rule.kind + rule.entity}><code>{ rule.entity }</code></li>);
             }
-            return <ul>{tiles}</ul>;
+            return <ul>{ tiles }</ul>;
         };
 
         Modal.createTrackedDialog('View Mjolnir list rules', '', QuestionDialog, {
             title: _t("Ban list rules - %(roomName)s", { roomName: name }),
             description: (
                 <div>
-                    <h3>{_t("Server rules")}</h3>
-                    {renderRules(list.serverRules)}
-                    <h3>{_t("User rules")}</h3>
-                    {renderRules(list.userRules)}
+                    <h3>{ _t("Server rules") }</h3>
+                    { renderRules(list.serverRules) }
+                    <h3>{ _t("User rules") }</h3>
+                    { renderRules(list.userRules) }
                 </div>
             ),
             button: _t("Close"),
@@ -167,7 +167,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
     private renderPersonalBanListRules() {
         const list = Mjolnir.sharedInstance().getPersonalList();
         const rules = list ? [...list.userRules, ...list.serverRules] : [];
-        if (!list || rules.length <= 0) return <i>{_t("You have not ignored anyone.")}</i>;
+        if (!list || rules.length <= 0) return <i>{ _t("You have not ignored anyone.") }</i>;
 
         const tiles = [];
         for (const rule of rules) {
@@ -178,17 +178,17 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
                         onClick={() => this.removePersonalRule(rule)}
                         disabled={this.state.busy}
                     >
-                        {_t("Remove")}
+                        { _t("Remove") }
                     </AccessibleButton>&nbsp;
-                    <code>{rule.entity}</code>
+                    <code>{ rule.entity }</code>
                 </li>,
             );
         }
 
         return (
             <div>
-                <p>{_t("You are currently ignoring:")}</p>
-                <ul>{tiles}</ul>
+                <p>{ _t("You are currently ignoring:") }</p>
+                <ul>{ tiles }</ul>
             </div>
         );
     }
@@ -198,12 +198,12 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
         const lists = Mjolnir.sharedInstance().lists.filter(b => {
             return personalList? personalList.roomId !== b.roomId : true;
         });
-        if (!lists || lists.length <= 0) return <i>{_t("You are not subscribed to any lists")}</i>;
+        if (!lists || lists.length <= 0) return <i>{ _t("You are not subscribed to any lists") }</i>;
 
         const tiles = [];
         for (const list of lists) {
             const room = MatrixClientPeg.get().getRoom(list.roomId);
-            const name = room ? <span>{room.name} (<code>{list.roomId}</code>)</span> : <code>list.roomId</code>;
+            const name = room ? <span>{ room.name } (<code>{ list.roomId }</code>)</span> : <code>list.roomId</code>;
             tiles.push(
                 <li key={list.roomId} className="mx_MjolnirUserSettingsTab_listItem">
                     <AccessibleButton
@@ -211,24 +211,24 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
                         onClick={() => this.unsubscribeFromList(list)}
                         disabled={this.state.busy}
                     >
-                        {_t("Unsubscribe")}
+                        { _t("Unsubscribe") }
                     </AccessibleButton>&nbsp;
                     <AccessibleButton
                         kind="primary_sm"
                         onClick={() => this.viewListRules(list)}
                         disabled={this.state.busy}
                     >
-                        {_t("View rules")}
+                        { _t("View rules") }
                     </AccessibleButton>&nbsp;
-                    {name}
+                    { name }
                 </li>,
             );
         }
 
         return (
             <div>
-                <p>{_t("You are currently subscribed to:")}</p>
-                <ul>{tiles}</ul>
+                <p>{ _t("You are currently subscribed to:") }</p>
+                <ul>{ tiles }</ul>
             </div>
         );
     }
@@ -238,37 +238,37 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
 
         return (
             <div className="mx_SettingsTab mx_MjolnirUserSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Ignored users")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Ignored users") }</div>
                 <div className="mx_SettingsTab_section">
                     <div className='mx_SettingsTab_subsectionText'>
-                        <span className='warning'>{_t("⚠ These settings are meant for advanced users.")}</span><br />
+                        <span className='warning'>{ _t("⚠ These settings are meant for advanced users.") }</span><br />
                         <br />
-                        {_t(
+                        { _t(
                             "Add users and servers you want to ignore here. Use asterisks " +
                             "to have %(brand)s match any characters. For example, <code>@bot:*</code> " +
                             "would ignore all users that have the name 'bot' on any server.",
-                            { brand }, { code: (s) => <code>{s}</code> },
-                        )}<br />
+                            { brand }, { code: (s) => <code>{ s }</code> },
+                        ) }<br />
                         <br />
-                        {_t(
+                        { _t(
                             "Ignoring people is done through ban lists which contain rules for " +
                             "who to ban. Subscribing to a ban list means the users/servers blocked by " +
                             "that list will be hidden from you.",
-                        )}
+                        ) }
                     </div>
                 </div>
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Personal ban list")}</span>
+                    <span className="mx_SettingsTab_subheading">{ _t("Personal ban list") }</span>
                     <div className='mx_SettingsTab_subsectionText'>
-                        {_t(
+                        { _t(
                             "Your personal ban list holds all the users/servers you personally don't " +
                             "want to see messages from. After ignoring your first user/server, a new room " +
                             "will show up in your room list named 'My Ban List' - stay in this room to keep " +
                             "the ban list in effect.",
-                        )}
+                        ) }
                     </div>
                     <div>
-                        {this.renderPersonalBanListRules()}
+                        { this.renderPersonalBanListRules() }
                     </div>
                     <div>
                         <form onSubmit={this.onAddPersonalRule} autoComplete="off">
@@ -285,22 +285,22 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
                                 onClick={this.onAddPersonalRule}
                                 disabled={this.state.busy}
                             >
-                                {_t("Ignore")}
+                                { _t("Ignore") }
                             </AccessibleButton>
                         </form>
                     </div>
                 </div>
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Subscribed lists")}</span>
+                    <span className="mx_SettingsTab_subheading">{ _t("Subscribed lists") }</span>
                     <div className='mx_SettingsTab_subsectionText'>
-                        <span className='warning'>{_t("Subscribing to a ban list will cause you to join it!")}</span>
+                        <span className='warning'>{ _t("Subscribing to a ban list will cause you to join it!") }</span>
                         &nbsp;
-                        <span>{_t(
+                        <span>{ _t(
                             "If this isn't what you want, please use a different tool to ignore users.",
-                        )}</span>
+                        ) }</span>
                     </div>
                     <div>
-                        {this.renderSubscribedBanLists()}
+                        { this.renderSubscribedBanLists() }
                     </div>
                     <div>
                         <form onSubmit={this.onSubscribeList} autoComplete="off">
@@ -316,7 +316,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
                                 onClick={this.onSubscribeList}
                                 disabled={this.state.busy}
                             >
-                                {_t("Subscribe")}
+                                { _t("Subscribe") }
                             </AccessibleButton>
                         </form>
                     </div>
diff --git a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx
index a0f4e330bb..5717813ae1 100644
--- a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx
@@ -24,7 +24,7 @@ export default class NotificationUserSettingsTab extends React.Component {
     render() {
         return (
             <div className="mx_SettingsTab mx_NotificationUserSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Notifications") }</div>
                 <div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
                     <Notifications />
                 </div>
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
index c4140153a5..2e5db59d9b 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
@@ -224,53 +224,53 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
 
         return (
             <div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Preferences") }</div>
 
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
-                    {this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
+                    <span className="mx_SettingsTab_subheading">{ _t("Room list") }</span>
+                    { this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
                 </div>
 
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Keyboard shortcuts")}</span>
+                    <span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
                     <AccessibleButton className="mx_SettingsFlag" onClick={KeyboardShortcuts.toggleDialog}>
                         { _t("To view all keyboard shortcuts, click here.") }
                     </AccessibleButton>
-                    {this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
+                    { this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS) }
                 </div>
 
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Displaying time")}</span>
-                    {this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
+                    <span className="mx_SettingsTab_subheading">{ _t("Displaying time") }</span>
+                    { this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS) }
                 </div>
 
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
-                    {this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
+                    <span className="mx_SettingsTab_subheading">{ _t("Composer") }</span>
+                    { this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS) }
                 </div>
 
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Code blocks")}</span>
-                    {this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
+                    <span className="mx_SettingsTab_subheading">{ _t("Code blocks") }</span>
+                    { this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS) }
                 </div>
 
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Images, GIFs and videos")}</span>
-                    {this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
+                    <span className="mx_SettingsTab_subheading">{ _t("Images, GIFs and videos") }</span>
+                    { this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS) }
                 </div>
 
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
-                    {this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
+                    <span className="mx_SettingsTab_subheading">{ _t("Timeline") }</span>
+                    { this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS) }
                 </div>
 
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("General")}</span>
-                    {this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
-                    {minimizeToTrayOption}
-                    {autoHideMenuOption}
-                    {autoLaunchOption}
-                    {warnBeforeExitOption}
+                    <span className="mx_SettingsTab_subheading">{ _t("General") }</span>
+                    { this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS) }
+                    { minimizeToTrayOption }
+                    { autoHideMenuOption }
+                    { autoLaunchOption }
+                    { warnBeforeExitOption }
                     <Field
                         label={_t('Autocomplete delay (ms)')}
                         type='number'
diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
index a03598b21f..79d501e712 100644
--- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
@@ -215,10 +215,10 @@ export default class SecurityUserSettingsTab extends React.Component {
             importExportButtons = (
                 <div className='mx_SecurityUserSettingsTab_importExportButtons'>
                     <AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
-                        {_t("Export E2E room keys")}
+                        { _t("Export E2E room keys") }
                     </AccessibleButton>
                     <AccessibleButton kind='primary' onClick={this._onImportE2eKeysClicked}>
-                        {_t("Import E2E room keys")}
+                        { _t("Import E2E room keys") }
                     </AccessibleButton>
                 </div>
             );
@@ -235,19 +235,19 @@ export default class SecurityUserSettingsTab extends React.Component {
 
         return (
             <div className='mx_SettingsTab_section'>
-                <span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t("Cryptography") }</span>
                 <ul className='mx_SettingsTab_subsectionText mx_SecurityUserSettingsTab_deviceInfo'>
                     <li>
-                        <label>{_t("Session ID:")}</label>
-                        <span><code>{deviceId}</code></span>
+                        <label>{ _t("Session ID:") }</label>
+                        <span><code>{ deviceId }</code></span>
                     </li>
                     <li>
-                        <label>{_t("Session key:")}</label>
-                        <span><code><b>{identityKey}</b></code></span>
+                        <label>{ _t("Session key:") }</label>
+                        <span><code><b>{ identityKey }</b></code></span>
                     </li>
                 </ul>
-                {importExportButtons}
-                {noSendUnverifiedSetting}
+                { importExportButtons }
+                { noSendUnverifiedSetting }
             </div>
         );
     }
@@ -270,9 +270,9 @@ export default class SecurityUserSettingsTab extends React.Component {
 
         return (
             <div className='mx_SettingsTab_section'>
-                <span className='mx_SettingsTab_subheading'>{_t('Ignored users')}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t('Ignored users') }</span>
                 <div className='mx_SettingsTab_subsectionText'>
-                    {userIds}
+                    { userIds }
                 </div>
             </div>
         );
@@ -289,14 +289,14 @@ export default class SecurityUserSettingsTab extends React.Component {
         const onClickReject = this._onRejectAllInvitesClicked.bind(this, invitedRooms);
         return (
             <div className='mx_SettingsTab_section mx_SecurityUserSettingsTab_bulkOptions'>
-                <span className='mx_SettingsTab_subheading'>{_t('Bulk options')}</span>
+                <span className='mx_SettingsTab_subheading'>{ _t('Bulk options') }</span>
                 <AccessibleButton onClick={onClickAccept} kind='primary' disabled={this.state.managingInvites}>
-                    {_t("Accept all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt })}
+                    { _t("Accept all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt }) }
                 </AccessibleButton>
                 <AccessibleButton onClick={onClickReject} kind='danger' disabled={this.state.managingInvites}>
-                    {_t("Reject all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt })}
+                    { _t("Reject all %(invitedRooms)s invites", { invitedRooms: this.state.invitedRoomAmt }) }
                 </AccessibleButton>
-                {this.state.managingInvites ? <InlineSpinner /> : <div />}
+                { this.state.managingInvites ? <InlineSpinner /> : <div /> }
             </div>
         );
     }
@@ -309,7 +309,7 @@ export default class SecurityUserSettingsTab extends React.Component {
 
         const secureBackup = (
             <div className='mx_SettingsTab_section'>
-                <span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
+                <span className="mx_SettingsTab_subheading">{ _t("Secure Backup") }</span>
                 <div className='mx_SettingsTab_subsectionText'>
                     <SecureBackupPanel />
                 </div>
@@ -318,7 +318,7 @@ export default class SecurityUserSettingsTab extends React.Component {
 
         const eventIndex = (
             <div className="mx_SettingsTab_section">
-                <span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
+                <span className="mx_SettingsTab_subheading">{ _t("Message search") }</span>
                 <EventIndexPanel />
             </div>
         );
@@ -330,7 +330,7 @@ export default class SecurityUserSettingsTab extends React.Component {
         const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
         const crossSigning = (
             <div className='mx_SettingsTab_section'>
-                <span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
+                <span className="mx_SettingsTab_subheading">{ _t("Cross-signing") }</span>
                 <div className='mx_SettingsTab_subsectionText'>
                     <CrossSigningPanel />
                 </div>
@@ -348,19 +348,19 @@ export default class SecurityUserSettingsTab extends React.Component {
         let privacySection;
         if (Analytics.canEnable() || CountlyAnalytics.instance.canEnable()) {
             privacySection = <React.Fragment>
-                <div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Privacy") }</div>
                 <div className="mx_SettingsTab_section">
-                    <span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
+                    <span className="mx_SettingsTab_subheading">{ _t("Analytics") }</span>
                     <div className="mx_SettingsTab_subsectionText">
-                        {_t(
+                        { _t(
                             "%(brand)s collects anonymous analytics to allow us to improve the application.",
                             { brand },
-                        )}
+                        ) }
                         &nbsp;
-                        {_t("Privacy is important to us, so we don't collect any personal or " +
-                            "identifiable data for our analytics.")}
+                        { _t("Privacy is important to us, so we don't collect any personal or " +
+                            "identifiable data for our analytics.") }
                         <AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
-                            {_t("Learn more about how we use analytics.")}
+                            { _t("Learn more about how we use analytics.") }
                         </AccessibleButton>
                     </div>
                     <SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
@@ -377,11 +377,11 @@ export default class SecurityUserSettingsTab extends React.Component {
             // only show the section if there's something to show
             if (ignoreUsersPanel || invitesPanel || e2ePanel) {
                 advancedSection = <>
-                    <div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
+                    <div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
                     <div className="mx_SettingsTab_section">
-                        {ignoreUsersPanel}
-                        {invitesPanel}
-                        {e2ePanel}
+                        { ignoreUsersPanel }
+                        { invitesPanel }
+                        { e2ePanel }
                     </div>
                 </>;
             }
@@ -389,31 +389,31 @@ export default class SecurityUserSettingsTab extends React.Component {
 
         return (
             <div className="mx_SettingsTab mx_SecurityUserSettingsTab">
-                {warning}
-                <div className="mx_SettingsTab_heading">{_t("Where you’re logged in")}</div>
+                { warning }
+                <div className="mx_SettingsTab_heading">{ _t("Where you’re logged in") }</div>
                 <div className="mx_SettingsTab_section">
                     <span>
-                        {_t(
+                        { _t(
                             "Manage the names of and sign out of your sessions below or " +
                             "<a>verify them in your User Profile</a>.", {},
                             {
                                 a: sub => <AccessibleButton kind="link" onClick={this._onGoToUserProfileClick}>
-                                    {sub}
+                                    { sub }
                                 </AccessibleButton>,
                             },
-                        )}
+                        ) }
                     </span>
                     <div className='mx_SettingsTab_subsectionText'>
-                        {_t("A session's public name is visible to people you communicate with")}
+                        { _t("A session's public name is visible to people you communicate with") }
                         <DevicesPanel />
                     </div>
                 </div>
-                <div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Encryption") }</div>
                 <div className="mx_SettingsTab_section">
-                    {secureBackup}
-                    {eventIndex}
-                    {crossSigning}
-                    {this._renderCurrentDeviceInfo()}
+                    { secureBackup }
+                    { eventIndex }
+                    { crossSigning }
+                    { this._renderCurrentDeviceInfo() }
                 </div>
                 { privacySection }
                 { advancedSection }
diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx
index 86c32cc6cd..b28ec592dd 100644
--- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.tsx
@@ -130,7 +130,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
 
     private renderDeviceOptions(devices: Array<MediaDeviceInfo>, category: MediaDeviceKindEnum): Array<JSX.Element> {
         return devices.map((d) => {
-            return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{d.label}</option>);
+            return (<option key={`${category}-${d.deviceId}`} value={d.deviceId}>{ d.label }</option>);
         });
     }
 
@@ -159,9 +159,9 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
         if (!this.state.mediaDevices) {
             requestButton = (
                 <div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
-                    <p>{_t("Missing media permissions, click the button below to request.")}</p>
+                    <p>{ _t("Missing media permissions, click the button below to request.") }</p>
                     <AccessibleButton onClick={this.requestMediaPermissions} kind="primary">
-                        {_t("Request media permissions")}
+                        { _t("Request media permissions") }
                     </AccessibleButton>
                 </div>
             );
@@ -182,7 +182,7 @@ export default class VoiceUserSettingsTab extends React.Component<{}, IState> {
 
         return (
             <div className="mx_SettingsTab mx_VoiceUserSettingsTab">
-                <div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
+                <div className="mx_SettingsTab_heading">{ _t("Voice & Video") }</div>
                 <div className="mx_SettingsTab_section">
                     { requestButton }
                     { speakerDropdown }
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index 9cefbbd94c..60ebec0752 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -156,7 +156,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
         )) }
         { spaces.map((s, i) => (
             <Draggable key={s.roomId} draggableId={s.roomId} index={i}>
-                {(provided, snapshot) => (
+                { (provided, snapshot) => (
                     <SpaceItem
                         {...provided.draggableProps}
                         {...provided.dragHandleProps}
@@ -170,7 +170,7 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
                         isPanelCollapsed={isPanelCollapsed}
                         onExpand={() => setPanelCollapsed(false)}
                     />
-                )}
+                ) }
             </Draggable>
         )) }
         { children }
@@ -266,13 +266,13 @@ const SpacePanel = () => {
             SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
         }}>
             <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
-                {({ onKeyDownHandler }) => (
+                { ({ onKeyDownHandler }) => (
                     <ul
                         className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
                         onKeyDown={onKeyDownHandler}
                     >
                         <Droppable droppableId="top-level-spaces">
-                            {(provided, snapshot) => (
+                            { (provided, snapshot) => (
                                 <AutoHideScrollbar
                                     {...provided.droppableProps}
                                     wrappedRef={provided.innerRef}
@@ -297,7 +297,7 @@ const SpacePanel = () => {
                                         isNarrow={isPanelCollapsed}
                                     />
                                 </AutoHideScrollbar>
-                            )}
+                            ) }
                         </Droppable>
                         <AccessibleTooltipButton
                             className={classNames("mx_SpacePanel_toggleCollapse", { expanded: !isPanelCollapsed })}
@@ -306,7 +306,7 @@ const SpacePanel = () => {
                         />
                         { contextMenu }
                     </ul>
-                )}
+                ) }
             </RovingTabIndexProvider>
         </DragDropContext>
     );
diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
index 9f4e0ecea7..a43b180752 100644
--- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
+++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
@@ -123,7 +123,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
             </AccessibleButton>
         </div>
 
-        <span className="mx_SettingsTab_subheading">{_t("Leave Space")}</span>
+        <span className="mx_SettingsTab_subheading">{ _t("Leave Space") }</span>
         <div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
             <AccessibleButton
                 kind="danger"
diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
index b76d53be41..ec17551d93 100644
--- a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
+++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
@@ -123,7 +123,7 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
     let addressesSection;
     if (visibility !== SpaceVisibility.Private) {
         addressesSection = <>
-            <span className="mx_SettingsTab_subheading">{_t("Address")}</span>
+            <span className="mx_SettingsTab_subheading">{ _t("Address") }</span>
             <div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
                 <AliasSettings
                     roomId={space.roomId}
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index 486a988b93..cf48658169 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -457,7 +457,7 @@ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
     parents,
 }) => {
     return <ul className="mx_SpaceTreeLevel">
-        {spaces.map(s => {
+        { spaces.map(s => {
             return (<SpaceItem
                 key={s.roomId}
                 activeSpaces={activeSpaces}
@@ -465,7 +465,7 @@ const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
                 isNested={isNested}
                 parents={parents}
             />);
-        })}
+        }) }
     </ul>;
 };
 
diff --git a/src/components/views/terms/InlineTermsAgreement.tsx b/src/components/views/terms/InlineTermsAgreement.tsx
index 306dfea6b9..54c0258d37 100644
--- a/src/components/views/terms/InlineTermsAgreement.tsx
+++ b/src/components/views/terms/InlineTermsAgreement.tsx
@@ -91,7 +91,7 @@ export default class InlineTermsAgreement extends React.Component<IProps, IState
                     policyLink: () => {
                         return (
                             <a href={policy.url} rel='noreferrer noopener' target='_blank'>
-                                {policy.name}
+                                { policy.name }
                                 <span className='mx_InlineTermsAgreement_link' />
                             </a>
                         );
@@ -100,10 +100,10 @@ export default class InlineTermsAgreement extends React.Component<IProps, IState
             );
             rendered.push(
                 <div key={i} className='mx_InlineTermsAgreement_cbContainer'>
-                    <div>{introText}</div>
+                    <div>{ introText }</div>
                     <div className='mx_InlineTermsAgreement_checkbox'>
                         <StyledCheckbox onChange={() => this.togglePolicy(i)} checked={policy.checked}>
-                            {_t("Accept")}
+                            { _t("Accept") }
                         </StyledCheckbox>
                     </div>
                 </div>,
@@ -118,14 +118,14 @@ export default class InlineTermsAgreement extends React.Component<IProps, IState
 
         return (
             <div>
-                {this.props.introElement}
-                {this.renderCheckboxes()}
+                { this.props.introElement }
+                { this.renderCheckboxes() }
                 <AccessibleButton
                     onClick={this.onContinue}
                     disabled={hasUnchecked || this.state.busy}
                     kind="primary_sm"
                 >
-                    {_t("Continue")}
+                    { _t("Continue") }
                 </AccessibleButton>
             </div>
         );
diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx
index 78f45be899..4738e38c0d 100644
--- a/src/components/views/toasts/GenericToast.tsx
+++ b/src/components/views/toasts/GenericToast.tsx
@@ -41,16 +41,16 @@ const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
     onReject,
 }) => {
     const detailContent = detail ? <div className="mx_Toast_detail">
-        {detail}
+        { detail }
     </div> : null;
 
     return <div>
         <div className="mx_Toast_description">
-            {description}
-            {detailContent}
+            { description }
+            { detailContent }
         </div>
         <div className="mx_Toast_buttons" aria-live="off">
-            {onReject && rejectLabel && <AccessibleButton kind="danger_outline" onClick={onReject}>
+            { onReject && rejectLabel && <AccessibleButton kind="danger_outline" onClick={onReject}>
                 { rejectLabel }
             </AccessibleButton> }
             <AccessibleButton onClick={onAccept} kind="primary">
diff --git a/src/components/views/toasts/NonUrgentEchoFailureToast.tsx b/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
index 906a56ef09..9d69922678 100644
--- a/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
+++ b/src/components/views/toasts/NonUrgentEchoFailureToast.tsx
@@ -31,11 +31,11 @@ export default class NonUrgentEchoFailureToast extends React.PureComponent {
         return (
             <div className="mx_NonUrgentEchoFailureToast">
                 <span className="mx_NonUrgentEchoFailureToast_icon" />
-                {_t("Your server isn't responding to some <a>requests</a>.", {}, {
+                { _t("Your server isn't responding to some <a>requests</a>.", {}, {
                     'a': (sub) => (
-                        <AccessibleButton kind="link" onClick={this.openDialog}>{sub}</AccessibleButton>
+                        <AccessibleButton kind="link" onClick={this.openDialog}>{ sub }</AccessibleButton>
                     ),
-                })}
+                }) }
             </div>
         );
     }
diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx
index 45f1464b0e..63e23bfdef 100644
--- a/src/components/views/toasts/VerificationRequestToast.tsx
+++ b/src/components/views/toasts/VerificationRequestToast.tsx
@@ -60,14 +60,14 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
                 this.setState({ counter });
             }, 1000);
         }
-        request.on("change", this._checkRequestIsPending);
+        request.on("change", this.checkRequestIsPending);
         // We should probably have a separate class managing the active verification toasts,
         // rather than monitoring this in the toast component itself, since we'll get problems
         // like the toasdt not going away when the verification is cancelled unless it's the
         // one on the top (ie. the one that's mounted).
         // As a quick & dirty fix, check the toast is still relevant when it mounts (this prevents
         // a toast hanging around after logging in if you did a verification as part of login).
-        this._checkRequestIsPending();
+        this.checkRequestIsPending();
 
         if (request.isSelfVerification) {
             const cli = MatrixClientPeg.get();
@@ -83,10 +83,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
     componentWillUnmount() {
         clearInterval(this.intervalHandle);
         const { request } = this.props;
-        request.off("change", this._checkRequestIsPending);
+        request.off("change", this.checkRequestIsPending);
     }
 
-    _checkRequestIsPending = () => {
+    private checkRequestIsPending = () => {
         const { request } = this.props;
         if (!request.canAccept) {
             ToastStore.sharedInstance().dismissToast(this.props.toastKey);
diff --git a/src/components/views/verification/VerificationCancelled.tsx b/src/components/views/verification/VerificationCancelled.tsx
index aa34b22382..c8c068f5eb 100644
--- a/src/components/views/verification/VerificationCancelled.tsx
+++ b/src/components/views/verification/VerificationCancelled.tsx
@@ -28,9 +28,9 @@ export default class VerificationCancelled extends React.Component<IProps> {
     public render(): React.ReactNode {
         const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
         return <div>
-            <p>{_t(
+            <p>{ _t(
                 "The other party cancelled the verification.",
-            )}</p>
+            ) }</p>
             <DialogButtons
                 primaryButton={_t('OK')}
                 hasCancel={false}
diff --git a/src/components/views/verification/VerificationComplete.tsx b/src/components/views/verification/VerificationComplete.tsx
index 7da601fc93..99cbab8d73 100644
--- a/src/components/views/verification/VerificationComplete.tsx
+++ b/src/components/views/verification/VerificationComplete.tsx
@@ -28,12 +28,12 @@ export default class VerificationComplete extends React.Component<IProps> {
     public render(): React.ReactNode {
         const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
         return <div>
-            <h2>{_t("Verified!")}</h2>
-            <p>{_t("You've successfully verified this user.")}</p>
-            <p>{_t(
+            <h2>{ _t("Verified!") }</h2>
+            <p>{ _t("You've successfully verified this user.") }</p>
+            <p>{ _t(
                 "Secure messages with this user are end-to-end encrypted and not able to be " +
                 "read by third parties.",
-            )}</p>
+            ) }</p>
             <DialogButtons onPrimaryButtonClick={this.props.onDone}
                 primaryButton={_t("Got It")}
                 hasCancel={false}
diff --git a/src/components/views/verification/VerificationShowSas.tsx b/src/components/views/verification/VerificationShowSas.tsx
index aaf0ca4848..71a947df49 100644
--- a/src/components/views/verification/VerificationShowSas.tsx
+++ b/src/components/views/verification/VerificationShowSas.tsx
@@ -81,14 +81,14 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
                         { emoji[0] }
                     </div>
                     <div className="mx_VerificationShowSas_emojiSas_label">
-                        {_t(capFirst(emoji[1]))}
+                        { _t(capFirst(emoji[1])) }
                     </div>
                 </div>,
             );
             sasDisplay = <div className="mx_VerificationShowSas_emojiSas">
-                {emojiBlocks.slice(0, 4)}
+                { emojiBlocks.slice(0, 4) }
                 <div className="mx_VerificationShowSas_emojiSas_break" />
-                {emojiBlocks.slice(4)}
+                { emojiBlocks.slice(4) }
             </div>;
             sasCaption = this.props.isSelf ?
                 _t(
@@ -99,10 +99,10 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
                 );
         } else if (this.props.sas.decimal) {
             const numberBlocks = this.props.sas.decimal.map((num, i) => <span key={i}>
-                {num}
+                { num }
             </span>);
             sasDisplay = <div className="mx_VerificationShowSas_decimalSas">
-                {numberBlocks}
+                { numberBlocks }
             </div>;
             sasCaption = this.props.isSelf ?
                 _t(
@@ -113,9 +113,9 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
                 );
         } else {
             return <div>
-                {_t("Unable to find a supported verification method.")}
+                { _t("Unable to find a supported verification method.") }
                 <AccessibleButton kind="primary" onClick={this.props.onCancel} className="mx_UserInfo_wideButton">
-                    {_t('Cancel')}
+                    { _t('Cancel') }
                 </AccessibleButton>
             </div>;
         }
@@ -165,12 +165,12 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
         }
 
         return <div className="mx_VerificationShowSas">
-            <p>{sasCaption}</p>
-            {sasDisplay}
-            <p>{this.props.isSelf ?
+            <p>{ sasCaption }</p>
+            { sasDisplay }
+            <p>{ this.props.isSelf ?
                 "":
-                _t("To be secure, do this in person or use a trusted way to communicate.")}</p>
-            {confirm}
+                _t("To be secure, do this in person or use a trusted way to communicate.") }</p>
+            { confirm }
         </div>;
     }
 }
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 64c101a284..8bdd6e0f55 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -465,7 +465,7 @@ export default class CallView extends React.Component<IProps, IState> {
         // in the near future, the dial pad button will go on the left. For now, it's the nothing button
         // because something needs to have margin-right: auto to make the alignment correct.
         const callControls = <div className={callControlsClasses}>
-            {dialpadButton}
+            { dialpadButton }
             <AccessibleButton
                 className={micClasses}
                 onClick={this.onMicMuteClick}
@@ -479,10 +479,10 @@ export default class CallView extends React.Component<IProps, IState> {
                     });
                 }}
             />
-            {vidMuteButton}
+            { vidMuteButton }
             <div className={micCacheClasses} />
             <div className={vidCacheClasses} />
-            {contextMenuButton}
+            { contextMenuButton }
         </div>;
 
         const avatarSize = this.props.pipMode ? 76 : 160;
@@ -506,16 +506,18 @@ export default class CallView extends React.Component<IProps, IState> {
             const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
 
             holdTransferContent = <div className="mx_CallView_holdTransferContent">
-                {_t(
+                { _t(
                     "Consulting with %(transferTarget)s. <a>Transfer to %(transferee)s</a>",
                     {
                         transferTarget: transferTargetName,
                         transferee: transfereeName,
                     },
                     {
-                        a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>{sub}</AccessibleButton>,
+                        a: sub => <AccessibleButton kind="link" onClick={this.onTransferClick}>
+                            { sub }
+                        </AccessibleButton>,
                     },
-                )}
+                ) }
             </div>;
         } else if (isOnHold) {
             let onHoldText = null;
@@ -524,7 +526,7 @@ export default class CallView extends React.Component<IProps, IState> {
                     _td("You held the call <a>Switch</a>") : _td("You held the call <a>Resume</a>");
                 onHoldText = _t(holdString, {}, {
                     a: sub => <AccessibleButton kind="link" onClick={this.onCallResumeClick}>
-                        {sub}
+                        { sub }
                     </AccessibleButton>,
                 });
             } else if (this.state.isLocalOnHold) {
@@ -533,7 +535,7 @@ export default class CallView extends React.Component<IProps, IState> {
                 });
             }
             holdTransferContent = <div className="mx_CallView_holdTransferContent">
-                {onHoldText}
+                { onHoldText }
             </div>;
         }
 
@@ -556,9 +558,9 @@ export default class CallView extends React.Component<IProps, IState> {
 
                 contentView = (
                     <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
-                        {onHoldBackground}
-                        {holdTransferContent}
-                        {callControls}
+                        { onHoldBackground }
+                        { holdTransferContent }
+                        { callControls }
                     </div>
                 );
             } else {
@@ -582,8 +584,8 @@ export default class CallView extends React.Component<IProps, IState> {
                                 />
                             </div>
                         </div>
-                        {holdTransferContent}
-                        {callControls}
+                        { holdTransferContent }
+                        { callControls }
                     </div>
                 );
             }
@@ -615,7 +617,7 @@ export default class CallView extends React.Component<IProps, IState> {
             // Saying "Connecting" here isn't really true, but the best thing
             // I can come up with, but this might be subject to change as well
             contentView = <div className={classes} onMouseMove={this.onMouseMove}>
-                {feeds}
+                { feeds }
                 <div className="mx_CallView_voice_avatarsContainer">
                     <div className="mx_CallView_voice_avatarContainer" style={{ width: avatarSize, height: avatarSize }}>
                         <RoomAvatar
@@ -625,8 +627,8 @@ export default class CallView extends React.Component<IProps, IState> {
                         />
                     </div>
                 </div>
-                <div className="mx_CallView_holdTransferContent">{_t("Connecting")}</div>
-                {callControls}
+                <div className="mx_CallView_holdTransferContent">{ _t("Connecting") }</div>
+                { callControls }
             </div>;
         } else {
             const containerClasses = classNames({
@@ -653,8 +655,8 @@ export default class CallView extends React.Component<IProps, IState> {
             });
 
             contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
-                {feeds}
-                {callControls}
+                { feeds }
+                { callControls }
             </div>;
         }
 
@@ -676,16 +678,16 @@ export default class CallView extends React.Component<IProps, IState> {
         }
 
         const headerControls = <div className="mx_CallView_header_controls">
-            {fullScreenButton}
-            {expandButton}
+            { fullScreenButton }
+            { expandButton }
         </div>;
 
         let header: React.ReactNode;
         if (!this.props.pipMode) {
             header = <div className="mx_CallView_header">
                 <div className="mx_CallView_header_phoneIcon"></div>
-                <span className="mx_CallView_header_callType">{callTypeText}</span>
-                {headerControls}
+                <span className="mx_CallView_header_callType">{ callTypeText }</span>
+                { headerControls }
             </div>;
             myClassName = 'mx_CallView_large';
         } else {
@@ -695,7 +697,7 @@ export default class CallView extends React.Component<IProps, IState> {
                     <AccessibleButton element='span' onClick={this.onSecondaryRoomAvatarClick}>
                         <RoomAvatar room={secCallRoom} height={16} width={16} />
                         <span className="mx_CallView_secondaryCall_roomName">
-                            {_t("%(name)s on hold", { name: secCallRoom.name })}
+                            { _t("%(name)s on hold", { name: secCallRoom.name }) }
                         </span>
                     </AccessibleButton>
                 </span>;
@@ -710,23 +712,23 @@ export default class CallView extends React.Component<IProps, IState> {
                         <RoomAvatar room={callRoom} height={32} width={32} />
                     </AccessibleButton>
                     <div className="mx_CallView_header_callInfo">
-                        <div className="mx_CallView_header_roomName">{callRoom.name}</div>
+                        <div className="mx_CallView_header_roomName">{ callRoom.name }</div>
                         <div className="mx_CallView_header_callTypeSmall">
-                            {callTypeText}
-                            {secondaryCallInfo}
+                            { callTypeText }
+                            { secondaryCallInfo }
                         </div>
                     </div>
-                    {headerControls}
+                    { headerControls }
                 </div>
             );
             myClassName = 'mx_CallView_pip';
         }
 
         return <div className={"mx_CallView " + myClassName}>
-            {header}
-            {contentView}
-            {dialPad}
-            {contextMenu}
+            { header }
+            { contentView }
+            { dialPad }
+            { contextMenu }
         </div>;
     }
 }
diff --git a/src/components/views/voip/DialPad.tsx b/src/components/views/voip/DialPad.tsx
index 6687c89b52..2af8bd6989 100644
--- a/src/components/views/voip/DialPad.tsx
+++ b/src/components/views/voip/DialPad.tsx
@@ -42,9 +42,9 @@ class DialPadButton extends React.PureComponent<IButtonProps> {
         switch (this.props.kind) {
             case DialPadButtonKind.Digit:
                 return <AccessibleButton className="mx_DialPad_button" onClick={this.onClick}>
-                    {this.props.digit}
+                    { this.props.digit }
                     <div className="mx_DialPad_buttonSubText">
-                        {this.props.digitSubtext}
+                        { this.props.digitSubtext }
                     </div>
                 </AccessibleButton>;
             case DialPadButtonKind.Dial:
@@ -80,7 +80,7 @@ export default class Dialpad extends React.PureComponent<IProps> {
         }
 
         return <div className="mx_DialPad">
-            {buttonNodes}
+            { buttonNodes }
         </div>;
     }
 }
diff --git a/src/components/views/voip/DialPadModal.tsx b/src/components/views/voip/DialPadModal.tsx
index 033aa2e700..0bba65e44f 100644
--- a/src/components/views/voip/DialPadModal.tsx
+++ b/src/components/views/voip/DialPadModal.tsx
@@ -101,7 +101,7 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
             </div>
             <div className="mx_DialPadModal_header">
                 <form onSubmit={this.onFormSubmit}>
-                    {dialPadField}
+                    { dialPadField }
                 </form>
             </div>
             <div className="mx_DialPadModal_dialPad">
diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx
index b6295f6b2c..95e97f1080 100644
--- a/src/components/views/voip/IncomingCallBox.tsx
+++ b/src/components/views/voip/IncomingCallBox.tsx
@@ -21,7 +21,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
 import dis from '../../../dispatcher/dispatcher';
 import { _t } from '../../../languageHandler';
 import { ActionPayload } from '../../../dispatcher/payloads';
-import CallHandler, { AudioID } from '../../../CallHandler';
+import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
 import RoomAvatar from '../avatars/RoomAvatar';
 import AccessibleButton from '../elements/AccessibleButton';
 import { CallState } from 'matrix-js-sdk/src/webrtc/call';
@@ -51,8 +51,13 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
         };
     }
 
+    componentDidMount = () => {
+        CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
+    };
+
     public componentWillUnmount() {
         dis.unregister(this.dispatcherRef);
+        CallHandler.sharedInstance().removeListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
     }
 
     private onAction = (payload: ActionPayload) => {
@@ -73,6 +78,12 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
         }
     };
 
+    private onSilencedCallsChanged = () => {
+        const callId = this.state.incomingCall?.callId;
+        if (!callId) return;
+        this.setState({ silenced: CallHandler.sharedInstance().isCallSilenced(callId) });
+    };
+
     private onAnswerClick: React.MouseEventHandler = (e) => {
         e.stopPropagation();
         dis.dispatch({
@@ -91,9 +102,10 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
 
     private onSilenceClick: React.MouseEventHandler = (e) => {
         e.stopPropagation();
-        const newState = !this.state.silenced;
-        this.setState({ silenced: newState });
-        newState ? CallHandler.sharedInstance().pause(AudioID.Ring) : CallHandler.sharedInstance().play(AudioID.Ring);
+        const callId = this.state.incomingCall.callId;
+        this.state.silenced ?
+            CallHandler.sharedInstance().unSilenceCall(callId):
+            CallHandler.sharedInstance().silenceCall(callId);
     };
 
     public render() {
@@ -133,8 +145,8 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
                     width={32}
                 />
                 <div>
-                    <h1>{caller}</h1>
-                    <p>{incomingCallText}</p>
+                    <h1>{ caller }</h1>
+                    <p>{ incomingCallText }</p>
                 </div>
                 <AccessibleTooltipButton
                     className={silenceClass}
@@ -144,7 +156,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
             </div>
             <div className="mx_IncomingCallBox_buttons">
                 <AccessibleButton
-                    className={"mx_IncomingCallBox_decline"}
+                    className="mx_IncomingCallBox_decline"
                     onClick={this.onRejectClick}
                     kind="danger"
                 >
@@ -152,7 +164,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
                 </AccessibleButton>
                 <div className="mx_IncomingCallBox_spacer" />
                 <AccessibleButton
-                    className={"mx_IncomingCallBox_accept"}
+                    className="mx_IncomingCallBox_accept"
                     onClick={this.onAnswerClick}
                     kind="primary"
                 >
diff --git a/src/createRoom.ts b/src/createRoom.ts
index afbeb7fdb9..effc6ec1ac 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -17,6 +17,7 @@ limitations under the License.
 
 import { MatrixClient } from "matrix-js-sdk/src/client";
 import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
 import { EventType } from "matrix-js-sdk/src/@types/event";
 
 import { MatrixClientPeg } from './MatrixClientPeg';
@@ -247,11 +248,11 @@ export function findDMForUser(client: MatrixClient, userId: string): Room {
  * NOTE: this assumes you've just created the room and there's not been an opportunity
  * for other code to run, so we shouldn't miss RoomState.newMember when it comes by.
  */
-export async function _waitForMember(client: MatrixClient, roomId: string, userId: string, opts = { timeout: 1500 }) {
+export async function waitForMember(client: MatrixClient, roomId: string, userId: string, opts = { timeout: 1500 }) {
     const { timeout } = opts;
     let handler;
     return new Promise((resolve) => {
-        handler = function(_event, _roomstate, member) {
+        handler = function(_, __, member: RoomMember) { // eslint-disable-line @typescript-eslint/naming-convention
             if (member.userId !== userId) return;
             if (member.roomId !== roomId) return;
             resolve(true);
@@ -324,7 +325,7 @@ export async function ensureDMExists(client: MatrixClient, userId: string): Prom
         }
 
         roomId = await createRoom({ encryption, dmUserId: userId, spinner: false, andView: false });
-        await _waitForMember(client, roomId, userId);
+        await waitForMember(client, roomId, userId);
     }
     return roomId;
 }
diff --git a/src/editor/parts.ts b/src/editor/parts.ts
index 351df5062f..7bda8e1901 100644
--- a/src/editor/parts.ts
+++ b/src/editor/parts.ts
@@ -274,7 +274,7 @@ abstract class PillPart extends BasePart implements IPillPart {
     }
 
     // helper method for subclasses
-    _setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) {
+    protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) {
         const avatarBackground = `url('${avatarUrl}')`;
         const avatarLetter = `'${initialLetter}'`;
         // check if the value is changing,
@@ -354,7 +354,7 @@ class RoomPillPart extends PillPart {
             initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId);
             avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId);
         }
-        this._setAvatarVars(node, avatarUrl, initialLetter);
+        this.setAvatarVars(node, avatarUrl, initialLetter);
     }
 
     get type(): IPillPart["type"] {
@@ -399,7 +399,7 @@ class UserPillPart extends PillPart {
         if (avatarUrl === defaultAvatarUrl) {
             initialLetter = Avatar.getInitialLetter(name);
         }
-        this._setAvatarVars(node, avatarUrl, initialLetter);
+        this.setAvatarVars(node, avatarUrl, initialLetter);
     }
 
     get type(): IPillPart["type"] {
diff --git a/src/emoji.ts b/src/emoji.ts
index 7caeb06d21..321eae63f6 100644
--- a/src/emoji.ts
+++ b/src/emoji.ts
@@ -15,26 +15,23 @@ limitations under the License.
 */
 
 import EMOJIBASE from 'emojibase-data/en/compact.json';
+import SHORTCODES from 'emojibase-data/en/shortcodes/iamcal.json';
 
 export interface IEmoji {
     annotation: string;
-    group: number;
+    group?: number;
     hexcode: string;
-    order: number;
+    order?: number;
     shortcodes: string[];
-    tags: string[];
+    tags?: string[];
     unicode: string;
+    skins?: Omit<IEmoji, "shortcodes" | "tags">[]; // Currently unused
     emoticon?: string;
 }
 
-interface IEmojiWithFilterString extends IEmoji {
-    filterString?: string;
-}
-
 // The unicode is stored without the variant selector
-const UNICODE_TO_EMOJI = new Map<string, IEmojiWithFilterString>(); // not exported as gets for it are handled by getEmojiFromUnicode
-export const EMOTICON_TO_EMOJI = new Map<string, IEmojiWithFilterString>();
-export const SHORTCODE_TO_EMOJI = new Map<string, IEmojiWithFilterString>();
+const UNICODE_TO_EMOJI = new Map<string, IEmoji>(); // not exported as gets for it are handled by getEmojiFromUnicode
+export const EMOTICON_TO_EMOJI = new Map<string, IEmoji>();
 
 export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
 
@@ -62,17 +59,23 @@ export const DATA_BY_CATEGORY = {
     "flags": [],
 };
 
-const ZERO_WIDTH_JOINER = "\u200D";
-
 // Store various mappings from unicode/emoticon/shortcode to the Emoji objects
-EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
+export const EMOJI: IEmoji[] = EMOJIBASE.map((emojiData: Omit<IEmoji, "shortcodes">) => {
+    // If there's ever a gap in shortcode coverage, we fudge it by
+    // filling it in with the emoji's CLDR annotation
+    const shortcodeData = SHORTCODES[emojiData.hexcode] ??
+        [emojiData.annotation.toLowerCase().replace(/ /g, "_")];
+
+    const emoji: IEmoji = {
+        ...emojiData,
+        // Homogenize shortcodes by ensuring that everything is an array
+        shortcodes: typeof shortcodeData === "string" ? [shortcodeData] : shortcodeData,
+    };
+
     const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
     if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
         DATA_BY_CATEGORY[categoryId].push(emoji);
     }
-    // This is used as the string to match the query against when filtering emojis
-    emoji.filterString = (`${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` +
-        `${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase();
 
     // Add mapping from unicode to Emoji object
     // The 'unicode' field that we use in emojibase has either
@@ -88,12 +91,7 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
         EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji);
     }
 
-    if (emoji.shortcodes) {
-        // Add mapping from each shortcode to Emoji object
-        emoji.shortcodes.forEach(shortcode => {
-            SHORTCODE_TO_EMOJI.set(shortcode, emoji);
-        });
-    }
+    return emoji;
 });
 
 /**
@@ -107,5 +105,3 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
 function stripVariation(str) {
     return str.replace(/[\uFE00-\uFE0F]$/, "");
 }
-
-export const EMOJI: IEmoji[] = EMOJIBASE;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 2ec48f5d43..f081ec823b 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -541,22 +541,8 @@
     "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s changed the alternative addresses for this room.",
     "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.",
     "%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.",
-    "Someone": "Someone",
-    "(not supported by this browser)": "(not supported by this browser)",
-    "%(senderName)s answered the call.": "%(senderName)s answered the call.",
-    "(could not connect media)": "(could not connect media)",
-    "(connection failed)": "(connection failed)",
-    "(their device couldn't start the camera / microphone)": "(their device couldn't start the camera / microphone)",
-    "(an error occurred)": "(an error occurred)",
-    "(no answer)": "(no answer)",
-    "(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)",
-    "%(senderName)s ended the call.": "%(senderName)s ended the call.",
-    "%(senderName)s declined the call.": "%(senderName)s declined the call.",
-    "%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.",
-    "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)",
-    "%(senderName)s placed a video call.": "%(senderName)s placed a video call.",
-    "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s placed a video call. (not supported by this browser)",
     "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.",
+    "Someone": "Someone",
     "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.",
     "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s made future room history visible to all room members, from the point they are invited.",
     "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s made future room history visible to all room members, from the point they joined.",
@@ -823,6 +809,7 @@
     "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
     "Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
     "Show info about bridges in room settings": "Show info about bridges in room settings",
+    "New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)",
     "Font size": "Font size",
     "Use custom size": "Use custom size",
     "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
@@ -1245,6 +1232,10 @@
     "Custom theme URL": "Custom theme URL",
     "Add theme": "Add theme",
     "Theme": "Theme",
+    "Message layout": "Message layout",
+    "IRC": "IRC",
+    "Modern": "Modern",
+    "Message bubbles": "Message bubbles",
     "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
     "Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
     "Customise your appearance": "Customise your appearance",
@@ -1851,6 +1842,18 @@
     "You cancelled verification.": "You cancelled verification.",
     "Verification cancelled": "Verification cancelled",
     "Compare emoji": "Compare emoji",
+    "Connected": "Connected",
+    "This call has ended": "This call has ended",
+    "Could not connect media": "Could not connect media",
+    "Connection failed": "Connection failed",
+    "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone",
+    "An unknown error occurred": "An unknown error occurred",
+    "No answer": "No answer",
+    "Unknown failure: %(reason)s)": "Unknown failure: %(reason)s)",
+    "This call has failed": "This call has failed",
+    "You missed this call": "You missed this call",
+    "Call back": "Call back",
+    "The call is in an unknown state!": "The call is in an unknown state!",
     "Sunday": "Sunday",
     "Monday": "Monday",
     "Tuesday": "Tuesday",
@@ -1860,6 +1863,8 @@
     "Saturday": "Saturday",
     "Today": "Today",
     "Yesterday": "Yesterday",
+    "Downloading": "Downloading",
+    "Download": "Download",
     "View Source": "View Source",
     "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
     "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.",
@@ -1992,7 +1997,6 @@
     "Zoom in": "Zoom in",
     "Rotate Left": "Rotate Left",
     "Rotate Right": "Rotate Right",
-    "Download": "Download",
     "Information": "Information",
     "Language Dropdown": "Language Dropdown",
     "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index a15eb4a4b7..e7329e4f2e 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -67,7 +67,7 @@ export function getUserLanguage(): string {
 
 // Function which only purpose is to mark that a string is translatable
 // Does not actually do anything. It's helpful for automatic extraction of translatable strings
-export function _td(s: string): string {
+export function _td(s: string): string { // eslint-disable-line @typescript-eslint/naming-convention
     return s;
 }
 
@@ -132,6 +132,8 @@ export type TranslatedString = string | React.ReactNode;
  *
  * @return a React <span> component if any non-strings were used in substitutions, otherwise a string
  */
+// eslint-next-line @typescript-eslint/naming-convention
+// eslint-nexline @typescript-eslint/naming-convention
 export function _t(text: string, variables?: IVariables): string;
 export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
 export function _t(text: string, variables?: IVariables, tags?: Tags): TranslatedString {
@@ -151,7 +153,7 @@ export function _t(text: string, variables?: IVariables, tags?: Tags): Translate
         if (typeof substituted === 'string') {
             return `@@${text}##${substituted}@@`;
         } else {
-            return <span className='translated-string' data-orig-string={text}>{substituted}</span>;
+            return <span className='translated-string' data-orig-string={text}>{ substituted }</span>;
         }
     } else {
         return substituted;
diff --git a/src/mjolnir/Mjolnir.ts b/src/mjolnir/Mjolnir.ts
index 21616eece3..fd30909798 100644
--- a/src/mjolnir/Mjolnir.ts
+++ b/src/mjolnir/Mjolnir.ts
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { MatrixClientPeg } from "../MatrixClientPeg";
 import { ALL_RULE_TYPES, BanList } from "./BanList";
 import SettingsStore from "../settings/SettingsStore";
@@ -21,19 +22,17 @@ import { _t } from "../languageHandler";
 import dis from "../dispatcher/dispatcher";
 import { SettingLevel } from "../settings/SettingLevel";
 import { Preset } from "matrix-js-sdk/src/@types/partials";
+import { ActionPayload } from "../dispatcher/payloads";
 
 // TODO: Move this and related files to the js-sdk or something once finalized.
 
 export class Mjolnir {
-    static _instance: Mjolnir = null;
+    private static instance: Mjolnir = null;
 
-    _lists: BanList[] = [];
-    _roomIds: string[] = [];
-    _mjolnirWatchRef = null;
-    _dispatcherRef = null;
-
-    constructor() {
-    }
+    private _lists: BanList[] = []; // eslint-disable-line @typescript-eslint/naming-convention
+    private _roomIds: string[] = []; // eslint-disable-line @typescript-eslint/naming-convention
+    private mjolnirWatchRef: string = null;
+    private dispatcherRef: string = null;
 
     get roomIds(): string[] {
         return this._roomIds;
@@ -44,16 +43,16 @@ export class Mjolnir {
     }
 
     start() {
-        this._mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this._onListsChanged.bind(this));
+        this.mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this.onListsChanged.bind(this));
 
-        this._dispatcherRef = dis.register(this._onAction);
+        this.dispatcherRef = dis.register(this.onAction);
         dis.dispatch({
             action: 'do_after_sync_prepared',
             deferred_action: { action: 'setup_mjolnir' },
         });
     }
 
-    _onAction = (payload) => {
+    private onAction = (payload: ActionPayload) => {
         if (payload['action'] === 'setup_mjolnir') {
             console.log("Setting up Mjolnir: after sync");
             this.setup();
@@ -62,23 +61,23 @@ export class Mjolnir {
 
     setup() {
         if (!MatrixClientPeg.get()) return;
-        this._updateLists(SettingsStore.getValue("mjolnirRooms"));
-        MatrixClientPeg.get().on("RoomState.events", this._onEvent);
+        this.updateLists(SettingsStore.getValue("mjolnirRooms"));
+        MatrixClientPeg.get().on("RoomState.events", this.onEvent);
     }
 
     stop() {
-        if (this._mjolnirWatchRef) {
-            SettingsStore.unwatchSetting(this._mjolnirWatchRef);
-            this._mjolnirWatchRef = null;
+        if (this.mjolnirWatchRef) {
+            SettingsStore.unwatchSetting(this.mjolnirWatchRef);
+            this.mjolnirWatchRef = null;
         }
 
-        if (this._dispatcherRef) {
-            dis.unregister(this._dispatcherRef);
-            this._dispatcherRef = null;
+        if (this.dispatcherRef) {
+            dis.unregister(this.dispatcherRef);
+            this.dispatcherRef = null;
         }
 
         if (!MatrixClientPeg.get()) return;
-        MatrixClientPeg.get().removeListener("RoomState.events", this._onEvent);
+        MatrixClientPeg.get().removeListener("RoomState.events", this.onEvent);
     }
 
     async getOrCreatePersonalList(): Promise<BanList> {
@@ -132,20 +131,20 @@ export class Mjolnir {
         this._lists = this._lists.filter(b => b.roomId !== roomId);
     }
 
-    _onEvent = (event) => {
+    private onEvent = (event: MatrixEvent) => {
         if (!MatrixClientPeg.get()) return;
         if (!this._roomIds.includes(event.getRoomId())) return;
         if (!ALL_RULE_TYPES.includes(event.getType())) return;
 
-        this._updateLists(this._roomIds);
+        this.updateLists(this._roomIds);
     };
 
-    _onListsChanged(settingName, roomId, atLevel, newValue) {
+    private onListsChanged(settingName: string, roomId: string, atLevel: SettingLevel, newValue: string[]) {
         // We know that ban lists are only recorded at one level so we don't need to re-eval them
-        this._updateLists(newValue);
+        this.updateLists(newValue);
     }
 
-    _updateLists(listRoomIds: string[]) {
+    private updateLists(listRoomIds: string[]) {
         if (!MatrixClientPeg.get()) return;
 
         console.log("Updating Mjolnir ban lists to: " + listRoomIds);
@@ -182,10 +181,10 @@ export class Mjolnir {
     }
 
     static sharedInstance(): Mjolnir {
-        if (!Mjolnir._instance) {
-            Mjolnir._instance = new Mjolnir();
+        if (!Mjolnir.instance) {
+            Mjolnir.instance = new Mjolnir();
         }
-        return Mjolnir._instance;
+        return Mjolnir.instance;
     }
 }
 
diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts
index b629ddafd8..fd84f479ad 100644
--- a/src/rageshake/submit-rageshake.ts
+++ b/src/rageshake/submit-rageshake.ts
@@ -203,7 +203,7 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp
     const body = await collectBugReport(opts);
 
     progressCallback(_t("Uploading logs"));
-    await _submitReport(bugReportEndpoint, body, progressCallback);
+    await submitReport(bugReportEndpoint, body, progressCallback);
 }
 
 /**
@@ -289,10 +289,10 @@ export async function submitFeedback(
         body.append(k, extraData[k]);
     }
 
-    await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
+    await submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
 }
 
-function _submitReport(endpoint: string, body: FormData, progressCallback: (string) => void) {
+function submitReport(endpoint: string, body: FormData, progressCallback: (str: string) => void) {
     return new Promise<void>((resolve, reject) => {
         const req = new XMLHttpRequest();
         req.open("POST", endpoint);
diff --git a/src/settings/Layout.ts b/src/settings/Layout.ts
index 3a42b2b510..d4e1f06c0a 100644
--- a/src/settings/Layout.ts
+++ b/src/settings/Layout.ts
@@ -1,5 +1,6 @@
 /*
 Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
+Copyright 2021 Quirin Götz <codeworks@supercable.onl>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -19,7 +20,8 @@ import PropTypes from 'prop-types';
 /* TODO: This should be later reworked into something more generic */
 export enum Layout {
     IRC = "irc",
-    Group = "group"
+    Group = "group",
+    Bubble = "bubble",
 }
 
 /* We need this because multiple components are still using JavaScript */
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 830ea9e32e..f0bdb2e0e5 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -41,6 +41,7 @@ import { Layout } from "./Layout";
 import ReducedMotionController from './controllers/ReducedMotionController';
 import IncompatibleController from "./controllers/IncompatibleController";
 import SdkConfig from "../SdkConfig";
+import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
 
 // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
 const LEVELS_ROOM_SETTINGS = [
@@ -321,6 +322,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: _td("Show info about bridges in room settings"),
         default: false,
     },
+    "feature_new_layout_switcher": {
+        isFeature: true,
+        supportedLevels: LEVELS_FEATURE,
+        displayName: _td("New layout switcher (with message bubbles)"),
+        default: false,
+        controller: new NewLayoutSwitcherController(),
+    },
     "RoomList.backgroundImage": {
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
         default: null,
diff --git a/src/settings/controllers/NewLayoutSwitcherController.ts b/src/settings/controllers/NewLayoutSwitcherController.ts
new file mode 100644
index 0000000000..b1d6cac55e
--- /dev/null
+++ b/src/settings/controllers/NewLayoutSwitcherController.ts
@@ -0,0 +1,26 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import SettingController from "./SettingController";
+import { SettingLevel } from "../SettingLevel";
+import SettingsStore from "../SettingsStore";
+import { Layout } from "../Layout";
+
+export default class NewLayoutSwitcherController extends SettingController {
+    public onChange(level: SettingLevel, roomId: string, newValue: any) {
+        // On disabling switch back to Layout.Group if Layout.Bubble
+        if (!newValue && SettingsStore.getValue("layout") == Layout.Bubble) {
+            SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
+        }
+    }
+}
diff --git a/src/stores/GroupFilterOrderStore.js b/src/stores/GroupFilterOrderStore.js
index e6401f21f8..e81d1b81f7 100644
--- a/src/stores/GroupFilterOrderStore.js
+++ b/src/stores/GroupFilterOrderStore.js
@@ -49,7 +49,7 @@ class GroupFilterOrderStore extends Store {
         this.__emitChange();
     }
 
-    __onDispatch(payload) {
+    __onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
         switch (payload.action) {
             // Initialise state after initial sync
             case 'view_room': {
diff --git a/src/stores/LifecycleStore.ts b/src/stores/LifecycleStore.ts
index 5381fc58ed..7db50af7a1 100644
--- a/src/stores/LifecycleStore.ts
+++ b/src/stores/LifecycleStore.ts
@@ -44,7 +44,7 @@ class LifecycleStore extends Store<ActionPayload> {
         this.__emitChange();
     }
 
-    protected __onDispatch(payload: ActionPayload) {
+    protected __onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention
         switch (payload.action) {
             case 'do_after_sync_prepared':
                 this.setState({
diff --git a/src/stores/RightPanelStore.ts b/src/stores/RightPanelStore.ts
index 521d124bad..b6f91bf835 100644
--- a/src/stores/RightPanelStore.ts
+++ b/src/stores/RightPanelStore.ts
@@ -144,7 +144,7 @@ export default class RightPanelStore extends Store<ActionPayload> {
         this.__emitChange();
     }
 
-    __onDispatch(payload: ActionPayload) {
+    __onDispatch(payload: ActionPayload) { // eslint-disable-line @typescript-eslint/naming-convention
         switch (payload.action) {
             case 'view_room':
                 if (payload.room_id === this.lastRoomId) break; // skip this transition, probably a permalink
diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx
index 1a85ff59b1..f2a7c135a3 100644
--- a/src/stores/RoomViewStore.tsx
+++ b/src/stores/RoomViewStore.tsx
@@ -96,7 +96,7 @@ class RoomViewStore extends Store<ActionPayload> {
         this.__emitChange();
     }
 
-    __onDispatch(payload) {
+    __onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
         switch (payload.action) {
             // view_room:
             //      - room_alias:   '#somealias:matrix.org'
@@ -325,8 +325,8 @@ class RoomViewStore extends Store<ActionPayload> {
             msg = _t("There was an error joining the room");
         } else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
             msg = <div>
-                {_t("Sorry, your homeserver is too old to participate in this room.")}<br />
-                {_t("Please contact your homeserver administrator.")}
+                { _t("Sorry, your homeserver is too old to participate in this room.") }<br />
+                { _t("Please contact your homeserver administrator.") }
             </div>;
         } else if (err.httpStatus === 404) {
             const invitingUserId = this.getInvitingUserId(this.state.roomId);
diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts
index 99f24cfbe7..44ec173e08 100644
--- a/src/stores/room-list/MessagePreviewStore.ts
+++ b/src/stores/room-list/MessagePreviewStore.ts
@@ -63,7 +63,7 @@ const PREVIEWS = {
 const MAX_EVENTS_BACKWARDS = 50;
 
 // type merging ftw
-type TAG_ANY = "im.vector.any";
+type TAG_ANY = "im.vector.any"; // eslint-disable-line @typescript-eslint/naming-convention
 const TAG_ANY: TAG_ANY = "im.vector.any";
 
 interface IState {
diff --git a/src/toasts/ServerLimitToast.tsx b/src/toasts/ServerLimitToast.tsx
index f915077bf4..9a104f552e 100644
--- a/src/toasts/ServerLimitToast.tsx
+++ b/src/toasts/ServerLimitToast.tsx
@@ -37,7 +37,7 @@ export const showToast = (limitType: string, onHideToast: () => void, adminConta
         key: TOAST_KEY,
         title: _t("Warning"),
         props: {
-            description: <React.Fragment>{errorText} {contactText}</React.Fragment>,
+            description: <React.Fragment>{ errorText } { contactText }</React.Fragment>,
             acceptLabel: _t("Ok"),
             onAccept: () => {
                 hideToast();
diff --git a/src/toasts/UpdateToast.tsx b/src/toasts/UpdateToast.tsx
index eb35c41512..cb072705ce 100644
--- a/src/toasts/UpdateToast.tsx
+++ b/src/toasts/UpdateToast.tsx
@@ -51,7 +51,7 @@ export const showToast = (version: string, newVersion: string, releaseNotes?: st
         onAccept = () => {
             Modal.createTrackedDialog('Display release notes', '', QuestionDialog, {
                 title: _t("What's New"),
-                description: <pre>{releaseNotes}</pre>,
+                description: <pre>{ releaseNotes }</pre>,
                 button: _t("Update"),
                 onFinished: (update) => {
                     if (update && PlatformPeg.get()) {
diff --git a/src/usercontent/index.js b/src/usercontent/index.js
index 13f38cc31a..c03126ec80 100644
--- a/src/usercontent/index.js
+++ b/src/usercontent/index.js
@@ -1,6 +1,13 @@
+let hasCalled = false;
 function remoteRender(event) {
     const data = event.data;
 
+    // If we're handling secondary calls, start from scratch
+    if (hasCalled) {
+        document.body.replaceWith(document.createElement("BODY"));
+    }
+    hasCalled = true;
+
     const img = document.createElement("span"); // we'll mask it as an image
     img.id = "img";
 
diff --git a/src/utils/AutoDiscoveryUtils.tsx b/src/utils/AutoDiscoveryUtils.tsx
index 6c0c8b2e13..bad87db2b9 100644
--- a/src/utils/AutoDiscoveryUtils.tsx
+++ b/src/utils/AutoDiscoveryUtils.tsx
@@ -90,7 +90,7 @@ export default class AutoDiscoveryUtils {
                             href="https://github.com/vector-im/element-web/blob/master/docs/config.md"
                             target="_blank"
                             rel="noreferrer noopener"
-                        >{sub}</a>;
+                        >{ sub }</a>;
                     },
                 },
             );
@@ -130,8 +130,8 @@ export default class AutoDiscoveryUtils {
             serverErrorIsFatal: isFatalError,
             serverDeadError: (
                 <div>
-                    <strong>{title}</strong>
-                    <div>{body}</div>
+                    <strong>{ title }</strong>
+                    <div>{ body }</div>
                 </div>
             ),
         };
diff --git a/src/utils/ErrorUtils.tsx b/src/utils/ErrorUtils.tsx
index c39ee21f09..4253564ffd 100644
--- a/src/utils/ErrorUtils.tsx
+++ b/src/utils/ErrorUtils.tsx
@@ -44,7 +44,7 @@ export function messageForResourceLimitError(
 
     const linkSub = sub => {
         if (adminContact) {
-            return <a href={adminContact} target="_blank" rel="noreferrer noopener">{sub}</a>;
+            return <a href={adminContact} target="_blank" rel="noreferrer noopener">{ sub }</a>;
         } else {
             return sub;
         }
@@ -76,12 +76,12 @@ export function messageForSyncError(err: MatrixError | Error): ReactNode {
             },
         );
         return <div>
-            <div>{limitError}</div>
-            <div>{adminContact}</div>
+            <div>{ limitError }</div>
+            <div>{ adminContact }</div>
         </div>;
     } else {
         return <div>
-            {_t("Unable to connect to Homeserver. Retrying...")}
+            { _t("Unable to connect to Homeserver. Retrying...") }
         </div>;
     }
 }
diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts
index 849e546485..e2af1c7464 100644
--- a/src/utils/EventUtils.ts
+++ b/src/utils/EventUtils.ts
@@ -111,14 +111,19 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent): {
     let tileHandler = getHandlerTile(mxEvent);
 
     // Info messages are basically information about commands processed on a room
-    let isBubbleMessage = eventType.startsWith("m.key.verification") ||
-            (eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
-            (eventType === EventType.RoomCreate) ||
-            (eventType === EventType.RoomEncryption) ||
-            (tileHandler === "messages.MJitsiWidgetEvent");
+    let isBubbleMessage = (
+        eventType.startsWith("m.key.verification") ||
+        (eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
+        (eventType === EventType.RoomCreate) ||
+        (eventType === EventType.RoomEncryption) ||
+        (eventType === EventType.CallInvite) ||
+        (tileHandler === "messages.MJitsiWidgetEvent")
+    );
     let isInfoMessage = (
-        !isBubbleMessage && eventType !== EventType.RoomMessage &&
-            eventType !== EventType.Sticker && eventType !== EventType.RoomCreate
+        !isBubbleMessage &&
+        eventType !== EventType.RoomMessage &&
+        eventType !== EventType.Sticker &&
+        eventType !== EventType.RoomCreate
     );
 
     // If we're showing hidden events in the timeline, we should use the
diff --git a/src/utils/LazyValue.ts b/src/utils/LazyValue.ts
new file mode 100644
index 0000000000..9cdcda489a
--- /dev/null
+++ b/src/utils/LazyValue.ts
@@ -0,0 +1,59 @@
+/*
+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.
+*/
+
+/**
+ * Utility class for lazily getting a variable.
+ */
+export class LazyValue<T> {
+    private val: T;
+    private prom: Promise<T>;
+    private done = false;
+
+    public constructor(private getFn: () => Promise<T>) {
+    }
+
+    /**
+     * Whether or not a cached value is present.
+     */
+    public get present(): boolean {
+        // we use a tracking variable just in case the final value is falsey
+        return this.done;
+    }
+
+    /**
+     * Gets the value without invoking a get. May be undefined until the
+     * value is fetched properly.
+     */
+    public get cachedValue(): T {
+        return this.val;
+    }
+
+    /**
+     * Gets a promise which resolves to the value, eventually.
+     */
+    public get value(): Promise<T> {
+        if (this.prom) return this.prom;
+        this.prom = this.getFn();
+
+        // Fork the promise chain to avoid accidentally making it return undefined always.
+        this.prom.then(v => {
+            this.val = v;
+            this.done = true;
+        });
+
+        return this.prom;
+    }
+}
diff --git a/src/utils/MediaEventHelper.ts b/src/utils/MediaEventHelper.ts
new file mode 100644
index 0000000000..cf34d5dea4
--- /dev/null
+++ b/src/utils/MediaEventHelper.ts
@@ -0,0 +1,119 @@
+/*
+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 { MatrixEvent } from "matrix-js-sdk/src";
+import { LazyValue } from "./LazyValue";
+import { Media, mediaFromContent } from "../customisations/Media";
+import { decryptFile } from "./DecryptFile";
+import { IMediaEventContent } from "../customisations/models/IMediaEventContent";
+import { IDestroyable } from "./IDestroyable";
+import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
+
+// TODO: We should consider caching the blobs. https://github.com/vector-im/element-web/issues/17192
+
+export class MediaEventHelper implements IDestroyable {
+    // Either an HTTP or Object URL (when encrypted) to the media.
+    public readonly sourceUrl: LazyValue<string>;
+    public readonly thumbnailUrl: LazyValue<string>;
+
+    // Either the raw or decrypted (when encrypted) contents of the file.
+    public readonly sourceBlob: LazyValue<Blob>;
+    public readonly thumbnailBlob: LazyValue<Blob>;
+
+    public readonly media: Media;
+
+    public constructor(private event: MatrixEvent) {
+        this.sourceUrl = new LazyValue(this.prepareSourceUrl);
+        this.thumbnailUrl = new LazyValue(this.prepareThumbnailUrl);
+        this.sourceBlob = new LazyValue(this.fetchSource);
+        this.thumbnailBlob = new LazyValue(this.fetchThumbnail);
+
+        this.media = mediaFromContent(this.event.getContent());
+    }
+
+    public get fileName(): string {
+        return this.event.getContent<IMediaEventContent>().body || "download";
+    }
+
+    public destroy() {
+        if (this.media.isEncrypted) {
+            if (this.sourceUrl.present) URL.revokeObjectURL(this.sourceUrl.cachedValue);
+            if (this.thumbnailUrl.present) URL.revokeObjectURL(this.thumbnailUrl.cachedValue);
+        }
+    }
+
+    private prepareSourceUrl = async () => {
+        if (this.media.isEncrypted) {
+            const blob = await this.sourceBlob.value;
+            return URL.createObjectURL(blob);
+        } else {
+            return this.media.srcHttp;
+        }
+    };
+
+    private prepareThumbnailUrl = async () => {
+        if (this.media.isEncrypted) {
+            const blob = await this.thumbnailBlob.value;
+            return URL.createObjectURL(blob);
+        } else {
+            return this.media.thumbnailHttp;
+        }
+    };
+
+    private fetchSource = () => {
+        if (this.media.isEncrypted) {
+            return decryptFile(this.event.getContent<IMediaEventContent>().file);
+        }
+        return this.media.downloadSource().then(r => r.blob());
+    };
+
+    private fetchThumbnail = () => {
+        if (!this.media.hasThumbnail) return Promise.resolve(null);
+
+        if (this.media.isEncrypted) {
+            const content = this.event.getContent<IMediaEventContent>();
+            if (content.info?.thumbnail_file) {
+                return decryptFile(content.info.thumbnail_file);
+            } else {
+                // "Should never happen"
+                console.warn("Media claims to have thumbnail and is encrypted, but no thumbnail_file found");
+                return Promise.resolve(null);
+            }
+        }
+
+        return fetch(this.media.thumbnailHttp).then(r => r.blob());
+    };
+
+    public static isEligible(event: MatrixEvent): boolean {
+        if (!event) return false;
+        if (event.isRedacted()) return false;
+        if (event.getType() === EventType.Sticker) return true;
+        if (event.getType() !== EventType.RoomMessage) return false;
+
+        const content = event.getContent();
+        const mediaMsgTypes: string[] = [
+            MsgType.Video,
+            MsgType.Audio,
+            MsgType.Image,
+            MsgType.File,
+        ];
+        if (mediaMsgTypes.includes(content.msgtype)) return true;
+        if (typeof(content.url) === 'string') return true;
+
+        // Finally, it's probably not media
+        return false;
+    }
+}
diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx
index 304a340247..63e34eea7a 100644
--- a/src/widgets/CapabilityText.tsx
+++ b/src/widgets/CapabilityText.tsx
@@ -20,7 +20,7 @@ import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
 import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities";
 import React from "react";
 
-type GENERIC_WIDGET_KIND = "generic";
+type GENERIC_WIDGET_KIND = "generic"; // eslint-disable-line @typescript-eslint/naming-convention
 const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic";
 
 interface ISendRecvStaticCapText {
@@ -176,7 +176,7 @@ export class CapabilityText {
                         primary: _t("Send <b>%(eventType)s</b> events as you in this room", {
                             eventType: eventCap.eventType,
                         }, {
-                            b: sub => <b>{sub}</b>,
+                            b: sub => <b>{ sub }</b>,
                         }),
                         byline: CapabilityText.bylineFor(eventCap),
                     };
@@ -185,7 +185,7 @@ export class CapabilityText {
                         primary: _t("See <b>%(eventType)s</b> events posted to this room", {
                             eventType: eventCap.eventType,
                         }, {
-                            b: sub => <b>{sub}</b>,
+                            b: sub => <b>{ sub }</b>,
                         }),
                         byline: CapabilityText.bylineFor(eventCap),
                     };
@@ -196,7 +196,7 @@ export class CapabilityText {
                         primary: _t("Send <b>%(eventType)s</b> events as you in your active room", {
                             eventType: eventCap.eventType,
                         }, {
-                            b: sub => <b>{sub}</b>,
+                            b: sub => <b>{ sub }</b>,
                         }),
                         byline: CapabilityText.bylineFor(eventCap),
                     };
@@ -205,7 +205,7 @@ export class CapabilityText {
                         primary: _t("See <b>%(eventType)s</b> events posted to your active room", {
                             eventType: eventCap.eventType,
                         }, {
-                            b: sub => <b>{sub}</b>,
+                            b: sub => <b>{ sub }</b>,
                         }),
                         byline: CapabilityText.bylineFor(eventCap),
                     };
@@ -216,7 +216,7 @@ export class CapabilityText {
         // We don't have enough context to render this capability specially, so we'll present it as-is
         return {
             primary: _t("The <b>%(capability)s</b> capability", { capability }, {
-                b: sub => <b>{sub}</b>,
+                b: sub => <b>{ sub }</b>,
             }),
         };
     }
@@ -324,13 +324,13 @@ export class CapabilityText {
                         primary = _t("Send <b>%(msgtype)s</b> messages as you in this room", {
                             msgtype: eventCap.keyStr,
                         }, {
-                            b: sub => <b>{sub}</b>,
+                            b: sub => <b>{ sub }</b>,
                         });
                     } else {
                         primary = _t("Send <b>%(msgtype)s</b> messages as you in your active room", {
                             msgtype: eventCap.keyStr,
                         }, {
-                            b: sub => <b>{sub}</b>,
+                            b: sub => <b>{ sub }</b>,
                         });
                     }
                 } else {
@@ -338,13 +338,13 @@ export class CapabilityText {
                         primary = _t("See <b>%(msgtype)s</b> messages posted to this room", {
                             msgtype: eventCap.keyStr,
                         }, {
-                            b: sub => <b>{sub}</b>,
+                            b: sub => <b>{ sub }</b>,
                         });
                     } else {
                         primary = _t("See <b>%(msgtype)s</b> messages posted to your active room", {
                             msgtype: eventCap.keyStr,
                         }, {
-                            b: sub => <b>{sub}</b>,
+                            b: sub => <b>{ sub }</b>,
                         });
                     }
                 }
diff --git a/test/accessibility/RovingTabIndex-test.js b/test/accessibility/RovingTabIndex-test.js
index bead5c3158..72d4253710 100644
--- a/test/accessibility/RovingTabIndex-test.js
+++ b/test/accessibility/RovingTabIndex-test.js
@@ -48,7 +48,7 @@ const button4 = <Button key={4}>d</Button>;
 describe("RovingTabIndex", () => {
     it("RovingTabIndexProvider renders children as expected", () => {
         const wrapper = mount(<RovingTabIndexProvider>
-            {() => <div><span>Test</span></div>}
+            { () => <div><span>Test</span></div> }
         </RovingTabIndexProvider>);
         expect(wrapper.text()).toBe("Test");
         expect(wrapper.html()).toBe('<div><span>Test</span></div>');
@@ -56,11 +56,11 @@ describe("RovingTabIndex", () => {
 
     it("RovingTabIndexProvider works as expected with useRovingTabIndex", () => {
         const wrapper = mount(<RovingTabIndexProvider>
-            {() => <React.Fragment>
+            { () => <React.Fragment>
                 { button1 }
                 { button2 }
                 { button3 }
-            </React.Fragment>}
+            </React.Fragment> }
         </RovingTabIndexProvider>);
 
         // should begin with 0th being active
@@ -98,15 +98,15 @@ describe("RovingTabIndex", () => {
 
     it("RovingTabIndexProvider works as expected with RovingTabIndexWrapper", () => {
         const wrapper = mount(<RovingTabIndexProvider>
-            {() => <React.Fragment>
+            { () => <React.Fragment>
                 { button1 }
                 { button2 }
                 <RovingTabIndexWrapper>
-                    {({ onFocus, isActive, ref }) =>
+                    { ({ onFocus, isActive, ref }) =>
                         <button onFocus={onFocus} tabIndex={isActive ? 0 : -1} ref={ref}>.</button>
                     }
                 </RovingTabIndexWrapper>
-            </React.Fragment>}
+            </React.Fragment> }
         </RovingTabIndexProvider>);
 
         // should begin with 0th being active
diff --git a/test/components/structures/CallEventGrouper-test.ts b/test/components/structures/CallEventGrouper-test.ts
new file mode 100644
index 0000000000..5719d92902
--- /dev/null
+++ b/test/components/structures/CallEventGrouper-test.ts
@@ -0,0 +1,140 @@
+/*
+Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+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 { stubClient } from '../../test-utils';
+import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
+import { MatrixClient } from 'matrix-js-sdk';
+import { EventType } from "matrix-js-sdk/src/@types/event";
+import CallEventGrouper, { CustomCallState } from "../../../src/components/structures/CallEventGrouper";
+import { CallState } from "matrix-js-sdk/src/webrtc/call";
+
+const MY_USER_ID = "@me:here";
+const THEIR_USER_ID = "@they:here";
+
+let client: MatrixClient;
+
+describe('CallEventGrouper', () => {
+    beforeEach(() => {
+        stubClient();
+        client = MatrixClientPeg.get();
+        client.getUserId = () => {
+            return MY_USER_ID;
+        };
+    });
+
+    it("detects a missed call", () => {
+        const grouper = new CallEventGrouper();
+
+        grouper.add({
+            getContent: () => {
+                return {
+                    call_id: "callId",
+                };
+            },
+            getType: () => {
+                return EventType.CallInvite;
+            },
+            sender: {
+                userId: THEIR_USER_ID,
+            },
+        });
+
+        expect(grouper.state).toBe(CustomCallState.Missed);
+    });
+
+    it("detects an ended call", () => {
+        const grouperHangup = new CallEventGrouper();
+        const grouperReject = new CallEventGrouper();
+
+        grouperHangup.add({
+            getContent: () => {
+                return {
+                    call_id: "callId",
+                };
+            },
+            getType: () => {
+                return EventType.CallInvite;
+            },
+            sender: {
+                userId: MY_USER_ID,
+            },
+        });
+        grouperHangup.add({
+            getContent: () => {
+                return {
+                    call_id: "callId",
+                };
+            },
+            getType: () => {
+                return EventType.CallHangup;
+            },
+            sender: {
+                userId: THEIR_USER_ID,
+            },
+        });
+
+        grouperReject.add({
+            getContent: () => {
+                return {
+                    call_id: "callId",
+                };
+            },
+            getType: () => {
+                return EventType.CallInvite;
+            },
+            sender: {
+                userId: MY_USER_ID,
+            },
+        });
+        grouperReject.add({
+            getContent: () => {
+                return {
+                    call_id: "callId",
+                };
+            },
+            getType: () => {
+                return EventType.CallReject;
+            },
+            sender: {
+                userId: THEIR_USER_ID,
+            },
+        });
+
+        expect(grouperHangup.state).toBe(CallState.Ended);
+        expect(grouperReject.state).toBe(CallState.Ended);
+    });
+
+    it("detects call type", () => {
+        const grouper = new CallEventGrouper();
+
+        grouper.add({
+            getContent: () => {
+                return {
+                    call_id: "callId",
+                    offer: {
+                        sdp: "this is definitely an SDP m=video",
+                    },
+                };
+            },
+            getType: () => {
+                return EventType.CallInvite;
+            },
+        });
+
+        expect(grouper.isVoice).toBe(false);
+    });
+});
diff --git a/test/createRoom-test.js b/test/createRoom-test.js
index b9b7e7df01..11cb7edf5d 100644
--- a/test/createRoom-test.js
+++ b/test/createRoom-test.js
@@ -1,5 +1,5 @@
 import './skinned-sdk'; // Must be first for skinning to work
-import { _waitForMember, canEncryptToAllUsers } from '../src/createRoom';
+import { waitForMember, canEncryptToAllUsers } from '../src/createRoom';
 import { EventEmitter } from 'events';
 
 /* Shorter timeout, we've got tests to run */
@@ -13,7 +13,7 @@ describe("waitForMember", () => {
     });
 
     it("resolves with false if the timeout is reached", (done) => {
-        _waitForMember(client, "", "", { timeout: 0 }).then((r) => {
+        waitForMember(client, "", "", { timeout: 0 }).then((r) => {
             expect(r).toBe(false);
             done();
         });
@@ -22,7 +22,7 @@ describe("waitForMember", () => {
     it("resolves with false if the timeout is reached, even if other RoomState.newMember events fire", (done) => {
         const roomId = "!roomId:domain";
         const userId = "@clientId:domain";
-        _waitForMember(client, roomId, userId, { timeout }).then((r) => {
+        waitForMember(client, roomId, userId, { timeout }).then((r) => {
             expect(r).toBe(false);
             done();
         });
@@ -32,7 +32,7 @@ describe("waitForMember", () => {
     it("resolves with true if RoomState.newMember fires", (done) => {
         const roomId = "!roomId:domain";
         const userId = "@clientId:domain";
-        _waitForMember(client, roomId, userId, { timeout }).then((r) => {
+        waitForMember(client, roomId, userId, { timeout }).then((r) => {
             expect(r).toBe(true);
             expect(client.listeners("RoomState.newMember").length).toBe(0);
             done();
diff --git a/yarn.lock b/yarn.lock
index 5076858f3b..75f1440d2f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3027,15 +3027,15 @@ emoji-regex@^8.0.0:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
   integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 
-emojibase-data@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-5.1.1.tgz#0a0d63dd07ce1376b3d27642f28cafa46f651de6"
-  integrity sha512-za/ma5SfogHjwUmGFnDbTvSfm8GGFvFaPS27GPti16YZSp5EPgz+UDsZCATXvJGit+oRNBbG/FtybXHKi2UQgQ==
+emojibase-data@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-6.2.0.tgz#db6c75c36905284fa623f4aa5f468d2be6ed364a"
+  integrity sha512-SWKaXD2QeQs06IE7qfJftsI5924Dqzp+V9xaa5RzZIEWhmlrG6Jt2iKwfgOPHu+5S8MEtOI7GdpKsXj46chXOw==
 
-emojibase-regex@^4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-4.1.1.tgz#6e781aca520281600fe7a177f1582c33cf1fc545"
-  integrity sha512-KSigB1zQkNKFygLZ5bAfHs87LJa1ni8QTQtq8lc53Y74NF3Dk2r7kfa8MpooTO8JBb5Xz660X4tSjDB+I+7elA==
+emojibase-regex@^5.1.3:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-5.1.3.tgz#f0ef621ed6ec624becd2326f999fd4ea01b94554"
+  integrity sha512-gT8T9LxLA8VJdI+8KQtyykB9qKzd7WuUL3M2yw6y9tplFeufOUANg3UKVaKUvkMcRNvZsSElWhxcJrx8WPE12g==
 
 encoding@^0.1.11:
   version "0.1.13"
@@ -3242,7 +3242,7 @@ eslint-config-google@^0.14.0:
 
 "eslint-plugin-matrix-org@github:matrix-org/eslint-plugin-matrix-org#main":
   version "0.3.2"
-  resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/383a1e4a9ef7944c921efda0de2ac9635d45cb5c"
+  resolved "https://codeload.github.com/matrix-org/eslint-plugin-matrix-org/tar.gz/8529f1d77863db6327cf1a1a4fa65d06cc26f91b"
 
 eslint-plugin-react-hooks@^4.2.0:
   version "4.2.0"