diff --git a/.github/workflows/layered-build.yaml b/.github/workflows/layered-build.yaml
index c9d7e89a75..c8717667d7 100644
--- a/.github/workflows/layered-build.yaml
+++ b/.github/workflows/layered-build.yaml
@@ -5,6 +5,8 @@ on:
 jobs:
     build:
         runs-on: ubuntu-latest
+        env:
+          PR_NUMBER: ${{github.event.number}}
         steps:
             - uses: actions/checkout@v2
             - name: Build
diff --git a/.github/workflows/typecheck.yaml b/.github/workflows/typecheck.yaml
index 2e08418cf6..f6ab643958 100644
--- a/.github/workflows/typecheck.yaml
+++ b/.github/workflows/typecheck.yaml
@@ -5,6 +5,8 @@ on:
 jobs:
     build:
         runs-on: ubuntu-latest
+        env:
+          PR_NUMBER: ${{github.event.number}}
         steps:
             - uses: actions/checkout@v2
             - uses: c-hive/gha-yarn-cache@v2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42e186220f..9a445a4041 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,70 @@
+Changes in [3.30.0](https://github.com/vector-im/element-desktop/releases/tag/v3.30.0) (2021-09-14)
+===================================================================================================
+
+## ✨ Features
+ * Add bubble highlight styling ([\#6582](https://github.com/matrix-org/matrix-react-sdk/pull/6582)). Fixes vector-im/element-web#18295 and vector-im/element-web#18295. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * [Release] Add config option to turn on in-room event sending timing metrics  ([\#6773](https://github.com/matrix-org/matrix-react-sdk/pull/6773)).
+ * Create narrow mode for Composer ([\#6682](https://github.com/matrix-org/matrix-react-sdk/pull/6682)). Fixes vector-im/element-web#18533 and vector-im/element-web#18533.
+ * Prefer matrix.to alias links over room id in spaces & share ([\#6745](https://github.com/matrix-org/matrix-react-sdk/pull/6745)). Fixes vector-im/element-web#18796 and vector-im/element-web#18796.
+ * Stop automatic playback of voice messages if a non-voice message is encountered ([\#6728](https://github.com/matrix-org/matrix-react-sdk/pull/6728)). Fixes vector-im/element-web#18850 and vector-im/element-web#18850.
+ * Show call length during a call ([\#6700](https://github.com/matrix-org/matrix-react-sdk/pull/6700)). Fixes vector-im/element-web#18566 and vector-im/element-web#18566. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Serialize and retry mass-leave when leaving space ([\#6737](https://github.com/matrix-org/matrix-react-sdk/pull/6737)). Fixes vector-im/element-web#18789 and vector-im/element-web#18789.
+ * Improve form handling in and around space creation ([\#6739](https://github.com/matrix-org/matrix-react-sdk/pull/6739)). Fixes vector-im/element-web#18775 and vector-im/element-web#18775.
+ * Split autoplay GIFs and videos into different settings ([\#6726](https://github.com/matrix-org/matrix-react-sdk/pull/6726)). Fixes vector-im/element-web#5771 and vector-im/element-web#5771. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Add autoplay for voice messages ([\#6710](https://github.com/matrix-org/matrix-react-sdk/pull/6710)). Fixes vector-im/element-web#18804, vector-im/element-web#18715, vector-im/element-web#18714 vector-im/element-web#17961 and vector-im/element-web#18804.
+ * Allow to use basic html to format invite messages ([\#6703](https://github.com/matrix-org/matrix-react-sdk/pull/6703)). Fixes vector-im/element-web#15738 and vector-im/element-web#15738. Contributed by [skolmer](https://github.com/skolmer).
+ * Allow widgets, when eligible, to interact with more rooms as per MSC2762 ([\#6684](https://github.com/matrix-org/matrix-react-sdk/pull/6684)).
+ * Remove arbitrary limits from send/receive events for widgets ([\#6719](https://github.com/matrix-org/matrix-react-sdk/pull/6719)). Fixes vector-im/element-web#17994 and vector-im/element-web#17994.
+ * Reload suggested rooms if we see the state change down /sync ([\#6715](https://github.com/matrix-org/matrix-react-sdk/pull/6715)). Fixes vector-im/element-web#18761 and vector-im/element-web#18761.
+ * When creating private spaces, make the initial rooms restricted if supported ([\#6721](https://github.com/matrix-org/matrix-react-sdk/pull/6721)). Fixes vector-im/element-web#18722 and vector-im/element-web#18722.
+ * Threading exploration work ([\#6658](https://github.com/matrix-org/matrix-react-sdk/pull/6658)). Fixes vector-im/element-web#18532 and vector-im/element-web#18532.
+ * Default to `Don't leave any` when leaving a space ([\#6697](https://github.com/matrix-org/matrix-react-sdk/pull/6697)). Fixes vector-im/element-web#18592 and vector-im/element-web#18592. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Special case redaction event sending from widgets per MSC2762 ([\#6686](https://github.com/matrix-org/matrix-react-sdk/pull/6686)). Fixes vector-im/element-web#18573 and vector-im/element-web#18573.
+ * Add active speaker indicators ([\#6639](https://github.com/matrix-org/matrix-react-sdk/pull/6639)). Fixes vector-im/element-web#17627 and vector-im/element-web#17627. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Increase general app performance by optimizing layers ([\#6644](https://github.com/matrix-org/matrix-react-sdk/pull/6644)). Fixes vector-im/element-web#18730 and vector-im/element-web#18730. Contributed by [Palid](https://github.com/Palid).
+
+## 🐛 Bug Fixes
+ * Fix autocomplete not having y-scroll ([\#6802](https://github.com/matrix-org/matrix-react-sdk/pull/6802)).
+ * Fix emoji picker and stickerpicker not appearing correctly when opened ([\#6801](https://github.com/matrix-org/matrix-react-sdk/pull/6801)).
+ * Debounce read marker update on scroll ([\#6774](https://github.com/matrix-org/matrix-react-sdk/pull/6774)).
+ * Fix Space creation wizard go to my first room button behaviour ([\#6748](https://github.com/matrix-org/matrix-react-sdk/pull/6748)). Fixes vector-im/element-web#18764 and vector-im/element-web#18764.
+ * Fix scroll being stuck at bottom ([\#6751](https://github.com/matrix-org/matrix-react-sdk/pull/6751)). Fixes vector-im/element-web#18903 and vector-im/element-web#18903.
+ * Fix widgets not remembering identity verification when asked to. ([\#6742](https://github.com/matrix-org/matrix-react-sdk/pull/6742)). Fixes vector-im/element-web#15631 and vector-im/element-web#15631.
+ * Add missing pluralisation i18n strings for Spaces ([\#6738](https://github.com/matrix-org/matrix-react-sdk/pull/6738)). Fixes vector-im/element-web#18780 and vector-im/element-web#18780.
+ * Make ForgotPassword UX slightly more user friendly ([\#6636](https://github.com/matrix-org/matrix-react-sdk/pull/6636)). Fixes vector-im/element-web#11531 and vector-im/element-web#11531. Contributed by [Palid](https://github.com/Palid).
+ * Don't context switch room on SpaceStore ready as it can break permalinks ([\#6730](https://github.com/matrix-org/matrix-react-sdk/pull/6730)). Fixes vector-im/element-web#17974 and vector-im/element-web#17974.
+ * Fix explore rooms button not working during space creation wizard ([\#6729](https://github.com/matrix-org/matrix-react-sdk/pull/6729)). Fixes vector-im/element-web#18762 and vector-im/element-web#18762.
+ * Fix bug where one party's media would sometimes not be shown ([\#6731](https://github.com/matrix-org/matrix-react-sdk/pull/6731)).
+ * Only make the initial space rooms suggested by default ([\#6714](https://github.com/matrix-org/matrix-react-sdk/pull/6714)). Fixes vector-im/element-web#18760 and vector-im/element-web#18760.
+ * Replace fake username in EventTilePreview with a proper loading state ([\#6702](https://github.com/matrix-org/matrix-react-sdk/pull/6702)). Fixes vector-im/element-web#15897 and vector-im/element-web#15897. Contributed by [skolmer](https://github.com/skolmer).
+ * Don't send prehistorical events to widgets during decryption at startup ([\#6695](https://github.com/matrix-org/matrix-react-sdk/pull/6695)). Fixes vector-im/element-web#18060 and vector-im/element-web#18060.
+ * When creating subspaces properly set restricted join rule ([\#6725](https://github.com/matrix-org/matrix-react-sdk/pull/6725)). Fixes vector-im/element-web#18797 and vector-im/element-web#18797.
+ * Fix the Image View not openning for some pinned messages ([\#6723](https://github.com/matrix-org/matrix-react-sdk/pull/6723)). Fixes vector-im/element-web#18422 and vector-im/element-web#18422. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Show autocomplete sections vertically ([\#6722](https://github.com/matrix-org/matrix-react-sdk/pull/6722)). Fixes vector-im/element-web#18860 and vector-im/element-web#18860. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix EmojiPicker filtering to lower case emojibase data strings ([\#6717](https://github.com/matrix-org/matrix-react-sdk/pull/6717)). Fixes vector-im/element-web#18686 and vector-im/element-web#18686.
+ * Clear currentRoomId when viewing home page, fixing document title ([\#6716](https://github.com/matrix-org/matrix-react-sdk/pull/6716)). Fixes vector-im/element-web#18668 and vector-im/element-web#18668.
+ * Fix membership updates to Spaces not applying in real-time ([\#6713](https://github.com/matrix-org/matrix-react-sdk/pull/6713)). Fixes vector-im/element-web#18737 and vector-im/element-web#18737.
+ * Don't show a double stacked invite modals when inviting to Spaces ([\#6698](https://github.com/matrix-org/matrix-react-sdk/pull/6698)). Fixes vector-im/element-web#18745 and vector-im/element-web#18745. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Remove non-functional DuckDuckGo Autocomplete Provider ([\#6712](https://github.com/matrix-org/matrix-react-sdk/pull/6712)). Fixes vector-im/element-web#18778 and vector-im/element-web#18778.
+ * Filter members on `MemberList` load ([\#6708](https://github.com/matrix-org/matrix-react-sdk/pull/6708)). Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix improper voice messages being produced in Firefox and sometimes other browsers. ([\#6696](https://github.com/matrix-org/matrix-react-sdk/pull/6696)). Fixes vector-im/element-web#18587 and vector-im/element-web#18587.
+ * Fix client forgetting which capabilities a widget was approved for ([\#6685](https://github.com/matrix-org/matrix-react-sdk/pull/6685)). Fixes vector-im/element-web#18786 and vector-im/element-web#18786.
+ * Fix left panel widgets not remembering collapsed state ([\#6687](https://github.com/matrix-org/matrix-react-sdk/pull/6687)). Fixes vector-im/element-web#17803 and vector-im/element-web#17803.
+ * Fix changelog link colour back to blue ([\#6692](https://github.com/matrix-org/matrix-react-sdk/pull/6692)). Fixes vector-im/element-web#18726 and vector-im/element-web#18726.
+ * Soften codeblock border color ([\#6564](https://github.com/matrix-org/matrix-react-sdk/pull/6564)). Fixes vector-im/element-web#18367 and vector-im/element-web#18367. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Pause ringing more aggressively ([\#6691](https://github.com/matrix-org/matrix-react-sdk/pull/6691)). Fixes vector-im/element-web#18588 and vector-im/element-web#18588. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix command autocomplete ([\#6680](https://github.com/matrix-org/matrix-react-sdk/pull/6680)). Fixes vector-im/element-web#18670 and vector-im/element-web#18670. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Don't re-sort the room-list based on profile/status changes ([\#6595](https://github.com/matrix-org/matrix-react-sdk/pull/6595)). Fixes vector-im/element-web#110 and vector-im/element-web#110. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix codeblock formatting with syntax highlighting on ([\#6681](https://github.com/matrix-org/matrix-react-sdk/pull/6681)). Fixes vector-im/element-web#18739 vector-im/element-web#18365 and vector-im/element-web#18739. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Add padding to the Add button in the notification settings ([\#6665](https://github.com/matrix-org/matrix-react-sdk/pull/6665)). Fixes vector-im/element-web#18706 and vector-im/element-web#18706. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+
+Changes in [3.29.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.29.1) (2021-09-13)
+===================================================================================================
+
+## 🔒 SECURITY FIXES
+ * Fix a security issue with message key sharing. See https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing
+   for details.
+
 Changes in [3.29.0](https://github.com/vector-im/element-desktop/releases/tag/v3.29.0) (2021-08-31)
 ===================================================================================================
 
diff --git a/package.json b/package.json
index 9798503e9e..3e3d9383c4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "3.29.0",
+  "version": "3.30.0",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {
@@ -93,10 +93,10 @@
     "prop-types": "^15.7.2",
     "qrcode": "^1.4.4",
     "re-resizable": "^6.9.0",
-    "react": "^17.0.2",
+    "react": "17.0.2",
     "react-beautiful-dnd": "^13.1.0",
     "react-blurhash": "^0.1.3",
-    "react-dom": "^17.0.2",
+    "react-dom": "17.0.2",
     "react-focus-lock": "^2.5.0",
     "react-transition-group": "^4.4.1",
     "resize-observer-polyfill": "^1.5.1",
@@ -142,9 +142,9 @@
     "@types/pako": "^1.0.1",
     "@types/parse5": "^6.0.0",
     "@types/qrcode": "^1.3.5",
-    "@types/react": "^17.0.2",
+    "@types/react": "17.0.14",
     "@types/react-beautiful-dnd": "^13.0.0",
-    "@types/react-dom": "^17.0.2",
+    "@types/react-dom": "17.0.9",
     "@types/react-transition-group": "^4.4.0",
     "@types/sanitize-html": "^2.3.1",
     "@types/zxcvbn": "^4.4.0",
@@ -175,9 +175,12 @@
     "stylelint": "^13.9.0",
     "stylelint-config-standard": "^20.0.0",
     "stylelint-scss": "^3.18.0",
-    "typescript": "^4.1.3",
+    "typescript": "4.3.5",
     "walk": "^2.3.14"
   },
+  "resolutions": {
+    "@types/react": "17.0.14"
+  },
   "jest": {
     "testEnvironment": "./__test-utils__/environment.js",
     "testMatch": [
diff --git a/res/css/structures/_GroupFilterPanel.scss b/res/css/structures/_GroupFilterPanel.scss
index cc0e760031..ceea20ed79 100644
--- a/res/css/structures/_GroupFilterPanel.scss
+++ b/res/css/structures/_GroupFilterPanel.scss
@@ -73,12 +73,6 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
 .mx_GroupFilterPanel .mx_TagTile {
     // opacity: 0.5;
     position: relative;
-
-    .mx_BetaDot {
-        position: absolute;
-        right: -13px;
-        top: -11px;
-    }
 }
 
 .mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss
index fb0f7d10e1..b6219da9e4 100644
--- a/res/css/structures/_RoomDirectory.scss
+++ b/res/css/structures/_RoomDirectory.scss
@@ -183,3 +183,40 @@ limitations under the License.
         padding: 0;
     }
 }
+
+@media screen and (max-width: 700px) {
+    .mx_RoomDirectory_roomMemberCount {
+        padding: 0px;
+    }
+
+    .mx_AccessibleButton_kind_secondary {
+        padding: 0px !important;
+    }
+
+    .mx_RoomDirectory_join {
+        margin-left: 0px;
+    }
+
+    .mx_RoomDirectory_alias {
+        margin-top: 10px;
+        margin-bottom: 10px;
+    }
+
+    .mx_RoomDirectory_roomDescription {
+        padding-bottom: 0px;
+    }
+
+    .mx_RoomDirectory_name {
+        margin-bottom: 5px;
+    }
+
+    .mx_RoomDirectory_roomAvatar {
+        margin-top: 10px;
+    }
+
+    .mx_RoomDirectory_table {
+        grid-template-columns: auto;
+        row-gap: 14px;
+        margin-top: 5px;
+    }
+}
diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss
index bbb1867f16..30d421a351 100644
--- a/res/css/structures/_SpacePanel.scss
+++ b/res/css/structures/_SpacePanel.scss
@@ -103,6 +103,16 @@ $activeBorderColor: $secondary-content;
         }
     }
 
+    .mx_SpaceItem_new {
+        position: relative;
+
+        .mx_BetaDot {
+            position: absolute;
+            left: 33px;
+            top: -5px;
+        }
+    }
+
     .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
         margin-left: $gutterSize;
         min-width: 40px;
@@ -139,7 +149,6 @@ $activeBorderColor: $secondary-content;
         &:not(.mx_SpaceButton_narrow) {
             .mx_SpaceButton_selectionWrapper {
                 width: 100%;
-                padding-right: 16px;
                 overflow: hidden;
             }
         }
@@ -151,7 +160,6 @@ $activeBorderColor: $secondary-content;
             display: block;
             text-overflow: ellipsis;
             overflow: hidden;
-            padding-right: 8px;
             font-size: $font-14px;
             line-height: $font-18px;
         }
@@ -196,22 +204,17 @@ $activeBorderColor: $secondary-content;
         }
 
         &.mx_SpaceButton_new .mx_SpaceButton_icon {
-            background-color: $accent-color;
-            transition: all .1s ease-in-out; // TODO transition
+            background-color: $roomlist-button-bg-color;
 
             &::before {
-                background-color: #ffffff;
+                background-color: $primary-content;
                 mask-image: url('$(res)/img/element-icons/plus.svg');
                 transition: all .2s ease-in-out; // TODO transition
             }
         }
 
-        &.mx_SpaceButton_newCancel .mx_SpaceButton_icon {
-            background-color: $icon-button-color;
-
-            &::before {
-                transform: rotate(45deg);
-            }
+        &.mx_SpaceButton_newCancel .mx_SpaceButton_icon::before {
+            transform: rotate(45deg);
         }
 
         .mx_BaseAvatar_image {
@@ -225,8 +228,7 @@ $activeBorderColor: $secondary-content;
             margin-top: auto;
             margin-bottom: auto;
             display: none;
-            position: absolute;
-            right: 4px;
+            position: relative;
 
             &::before {
                 top: 2px;
@@ -245,8 +247,6 @@ $activeBorderColor: $secondary-content;
     }
 
     .mx_SpacePanel_badgeContainer {
-        position: absolute;
-
         // Create a flexbox to make aligning dot badges easier
         display: flex;
         align-items: center;
@@ -264,6 +264,7 @@ $activeBorderColor: $secondary-content;
     &.collapsed {
         .mx_SpaceButton {
             .mx_SpacePanel_badgeContainer {
+                position: absolute;
                 right: 0;
                 top: 0;
 
@@ -293,19 +294,12 @@ $activeBorderColor: $secondary-content;
     }
 
     &:not(.collapsed) {
-        .mx_SpacePanel_badgeContainer {
-            position: absolute;
-            right: 4px;
-        }
-
         .mx_SpaceButton:hover,
         .mx_SpaceButton:focus-within,
         .mx_SpaceButton_hasMenuOpen {
             &:not(.mx_SpaceButton_invite) {
                 // Hide the badge container on hover because it'll be a menu button
                 .mx_SpacePanel_badgeContainer {
-                    width: 0;
-                    height: 0;
                     display: none;
                 }
 
diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 39eabe2e07..812b6dcea9 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -215,9 +215,10 @@ $SpaceRoomViewInnerWidth: 428px;
             }
         }
 
-        > .mx_BaseAvatar_image,
-        > .mx_BaseAvatar > .mx_BaseAvatar_image {
-            border-radius: 12px;
+        > .mx_RoomAvatar_isSpaceRoom {
+            &.mx_BaseAvatar_image, .mx_BaseAvatar_image {
+                border-radius: 12px;
+            }
         }
 
         h1.mx_SpaceRoomView_preview_name {
diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss
index ff6910852c..a6b61d3ead 100644
--- a/res/css/views/beta/_BetaCard.scss
+++ b/res/css/views/beta/_BetaCard.scss
@@ -113,6 +113,7 @@ $dot-size: 12px;
     animation: mx_Beta_bluePulse 2s infinite;
     animation-iteration-count: 20;
     position: relative;
+    pointer-events: none;
 
     &::after {
         content: "";
diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss
index d74c985d4c..71d37a015d 100644
--- a/res/css/views/elements/_Field.scss
+++ b/res/css/views/elements/_Field.scss
@@ -98,14 +98,14 @@ limitations under the License.
     transition:
         font-size 0.25s ease-out 0.1s,
         color 0.25s ease-out 0.1s,
-        top 0.25s ease-out 0.1s,
+        transform 0.25s ease-out 0.1s,
         background-color 0.25s ease-out 0.1s;
     color: $primary-content;
     background-color: transparent;
     font-size: $font-14px;
+    transform: translateY(0);
     position: absolute;
     left: 0px;
-    top: 0px;
     margin: 7px 8px;
     padding: 2px;
     pointer-events: none; // Allow clicks to fall through to the input
@@ -124,10 +124,10 @@ limitations under the License.
     transition:
         font-size 0.25s ease-out 0s,
         color 0.25s ease-out 0s,
-        top 0.25s ease-out 0s,
+        transform 0.25s ease-out 0s,
         background-color 0.25s ease-out 0s;
     font-size: $font-10px;
-    top: -13px;
+    transform: translateY(-13px);
     padding: 0 2px;
     background-color: $field-focused-label-bg-color;
     pointer-events: initial;
diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss
index 8d2b338d9d..fcdab37f5a 100644
--- a/res/css/views/rooms/_Autocomplete.scss
+++ b/res/css/views/rooms/_Autocomplete.scss
@@ -7,7 +7,6 @@
     background: $background;
     border-bottom: none;
     border-radius: 8px 8px 0 0;
-    max-height: 35vh;
     overflow: clip;
     display: flex;
     flex-direction: column;
@@ -64,6 +63,7 @@
     margin: 12px;
     height: 100%;
     overflow-y: scroll;
+    max-height: 35vh;
 }
 
 .mx_Autocomplete_Completion_container_truncate {
diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss
index 41c9dad394..389a5c9706 100644
--- a/res/css/views/rooms/_EventBubbleTile.scss
+++ b/res/css/views/rooms/_EventBubbleTile.scss
@@ -23,11 +23,11 @@ limitations under the License.
 }
 
 .mx_EventTile[data-layout=bubble] {
-
     position: relative;
     margin-top: var(--gutterSize);
-    margin-left: 50px;
+    margin-left: 49px;
     margin-right: 100px;
+    font-size: $font-14px;
 
     &.mx_EventTile_continuation {
         margin-top: 2px;
@@ -77,10 +77,11 @@ limitations under the License.
         max-width: 70%;
     }
 
-    .mx_SenderProfile {
+    > .mx_SenderProfile {
         position: relative;
         top: -2px;
         left: 2px;
+        font-size: $font-15px;
     }
 
     &[data-self=false] {
@@ -113,8 +114,6 @@ limitations under the License.
 
         .mx_ReplyTile .mx_SenderProfile {
             display: block;
-            top: unset;
-            left: unset;
         }
 
         .mx_ReactionsRow {
@@ -287,6 +286,8 @@ limitations under the License.
     .mx_EventTile_line,
     .mx_EventTile_info {
         min-width: 100%;
+        // Preserve alignment with left edge of text in bubbles
+        margin: 0;
     }
 
     .mx_EventTile_e2eIcon {
@@ -294,9 +295,10 @@ limitations under the License.
     }
 
     .mx_EventTile_line > a {
+        // Align timestamps with those of normal bubble tiles
         right: auto;
-        top: -15px;
-        left: -68px;
+        top: -11px;
+        left: -95px;
     }
 }
 
@@ -326,11 +328,10 @@ limitations under the License.
     }
 
     .mx_EventTile_line {
-        margin: 0 5px;
+        margin: 0;
         > a {
-            left: auto;
-            right: 0;
-            transform: translateX(calc(100% + 5px));
+            // Align timestamps with those of normal bubble tiles
+            left: -76px;
         }
     }
 
@@ -340,7 +341,8 @@ limitations under the License.
 }
 
 .mx_EventListSummary[data-expanded=false][data-layout=bubble] {
-    padding: 0 34px;
+    // Align with left edge of bubble tiles
+    padding: 0 49px;
 }
 
 /* events that do not require bubble layout */
diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss
index 3fffbfd64c..6db2185dd5 100644
--- a/res/css/views/rooms/_RoomSublist.scss
+++ b/res/css/views/rooms/_RoomSublist.scss
@@ -172,14 +172,12 @@ limitations under the License.
         }
     }
 
-    // In the general case, we leave height of headers alone even if sticky, so
-    // that the sublists below them do not jump. However, that leaves a gap
-    // when scrolled to the top above the first sublist (whose header can only
-    // ever stick to top), so we force height to 0 for only that first header.
-    // See also https://github.com/vector-im/element-web/issues/14429.
-    &:first-child .mx_RoomSublist_headerContainer {
-        height: 0;
-        padding-bottom: 4px;
+    // In the general case, we reserve space for each sublist header to prevent
+    // scroll jumps when they become sticky. However, that leaves a gap when
+    // scrolled to the top above the first sublist (whose header can only ever
+    // stick to top), so we make sure to exclude the first visible sublist.
+    &:not(.mx_RoomSublist_hidden) ~ .mx_RoomSublist .mx_RoomSublist_headerContainer {
+        height: 24px;
     }
 
     .mx_RoomSublist_resizeBox {
diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss
index d1076205ad..16f607c95f 100644
--- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss
@@ -21,6 +21,17 @@ limitations under the License.
 
     .mx_SettingsTab_section {
         margin-bottom: 30px;
+
+        > details {
+            > summary {
+                cursor: pointer;
+                color: $primary-content;
+            }
+
+            & + .mx_SettingsFlag {
+                margin-top: 20px;
+            }
+        }
     }
 
     .mx_PreferencesUserSettingsTab_CommunityMigrator {
diff --git a/res/img/betas/.gitkeep b/res/img/betas/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/res/img/betas/spaces.png b/res/img/betas/spaces.png
deleted file mode 100644
index f4cfa90b4e..0000000000
Binary files a/res/img/betas/spaces.png and /dev/null differ
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 4a6db5dd55..0bc61d438d 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -184,6 +184,9 @@ $visual-bell-bg-color: #800;
 
 $room-warning-bg-color: $header-panel-bg-color;
 
+$authpage-body-bg-color: $background;
+$authpage-primary-color: $primary-content;
+
 $dark-panel-bg-color: $header-panel-bg-color;
 $panel-gradient: rgba(34, 38, 46, 0), rgba(34, 38, 46, 1);
 
diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss
index f4685fe8fa..455798a556 100644
--- a/res/themes/light-custom/css/_custom.scss
+++ b/res/themes/light-custom/css/_custom.scss
@@ -82,6 +82,8 @@ $tab-label-fg-color: var(--timeline-text-color);
 // was #4e5054
 $authpage-lang-color: var(--timeline-text-color);
 $roomheader-color: var(--timeline-text-color);
+// was #232f32
+$authpage-primary-color: var(--timeline-text-color);
 // --roomlist-text-secondary-color
 $roomtile-preview-color: var(--roomlist-text-secondary-color);
 $roomlist-header-color: var(--roomlist-text-secondary-color);
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index e48fd52cb1..5f5aeb389f 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -574,11 +574,12 @@ async function doSetLoggedIn(
         await abortLogin();
     }
 
-    PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
-
     Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
 
     MatrixClientPeg.replaceUsingCreds(credentials);
+
+    PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
+
     const client = MatrixClientPeg.get();
 
     if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts
index 073f24523d..154f167745 100644
--- a/src/MediaDeviceHandler.ts
+++ b/src/MediaDeviceHandler.ts
@@ -17,8 +17,8 @@ limitations under the License.
 
 import SettingsStore from "./settings/SettingsStore";
 import { SettingLevel } from "./settings/SettingLevel";
-import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
 import EventEmitter from 'events';
+import { MatrixClientPeg } from "./MatrixClientPeg";
 
 // XXX: MediaDeviceKind is a union type, so we make our own enum
 export enum MediaDeviceKindEnum {
@@ -74,8 +74,8 @@ export default class MediaDeviceHandler extends EventEmitter {
         const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
         const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
 
-        setMatrixCallAudioInput(audioDeviceId);
-        setMatrixCallVideoInput(videoDeviceId);
+        MatrixClientPeg.get().getMediaHandler().setAudioInput(audioDeviceId);
+        MatrixClientPeg.get().getMediaHandler().setVideoInput(videoDeviceId);
     }
 
     public setAudioOutput(deviceId: string): void {
@@ -90,7 +90,7 @@ export default class MediaDeviceHandler extends EventEmitter {
      */
     public setAudioInput(deviceId: string): void {
         SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
-        setMatrixCallAudioInput(deviceId);
+        MatrixClientPeg.get().getMediaHandler().setAudioInput(deviceId);
     }
 
     /**
@@ -100,7 +100,7 @@ export default class MediaDeviceHandler extends EventEmitter {
      */
     public setVideoInput(deviceId: string): void {
         SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
-        setMatrixCallVideoInput(deviceId);
+        MatrixClientPeg.get().getMediaHandler().setVideoInput(deviceId);
     }
 
     public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts
index 860a155aff..c6e351b91a 100644
--- a/src/PosthogAnalytics.ts
+++ b/src/PosthogAnalytics.ts
@@ -18,6 +18,8 @@ import posthog, { PostHog } from 'posthog-js';
 import PlatformPeg from './PlatformPeg';
 import SdkConfig from './SdkConfig';
 import SettingsStore from './settings/SettingsStore';
+import { MatrixClientPeg } from "./MatrixClientPeg";
+import { MatrixClient } from "matrix-js-sdk/src/client";
 
 /* Posthog analytics tracking.
  *
@@ -27,10 +29,11 @@ import SettingsStore from './settings/SettingsStore';
  * - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is
  *   enabled, events are not sent (this detection is built into posthog and turned on via the
  *   `respect_dnt` flag being passed to `posthog.init`).
- * - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously, i.e.
- *   hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256.
- * - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e.
- *   redact all matrix identifiers in tracking events.
+ * - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously by maintaining
+ *   a randomised analytics ID in account_data for that user (shared between devices) and sending it to posthog to
+     identify the user.
+ * - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e. do not identify the user
+     using any identifier that would be consistent across devices.
  * - If both flags are false or not set, events are not sent.
  */
 
@@ -71,12 +74,6 @@ interface IPageView extends IAnonymousEvent {
     };
 }
 
-const hashHex = async (input: string): Promise<string> => {
-    const buf = new TextEncoder().encode(input);
-    const digestBuf = await window.crypto.subtle.digest("sha-256", buf);
-    return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join("");
-};
-
 const whitelistedScreens = new Set([
     "register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
     "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
@@ -89,7 +86,6 @@ export async function getRedactedCurrentLocation(
     anonymity: Anonymity,
 ): Promise<string> {
     // Redact PII from the current location.
-    // If anonymous is true, redact entirely, if false, substitute it with a hash.
     // For known screens, assumes a URL structure of /<screen name>/might/be/pii
     if (origin.startsWith('file://')) {
         pathname = "/<redacted_file_scheme_url>/";
@@ -99,17 +95,13 @@ export async function getRedactedCurrentLocation(
     if (hash == "") {
         hashStr = "";
     } else {
-        let [beforeFirstSlash, screen, ...parts] = hash.split("/");
+        let [beforeFirstSlash, screen] = hash.split("/");
 
         if (!whitelistedScreens.has(screen)) {
             screen = "<redacted_screen_name>";
         }
 
-        for (let i = 0; i < parts.length; i++) {
-            parts[i] = anonymity === Anonymity.Anonymous ? `<redacted>` : await hashHex(parts[i]);
-        }
-
-        hashStr = `${beforeFirstSlash}/${screen}/${parts.join("/")}`;
+        hashStr = `${beforeFirstSlash}/${screen}/<redacted>`;
     }
     return origin + pathname + hashStr;
 }
@@ -123,15 +115,15 @@ export class PosthogAnalytics {
     /* Wrapper for Posthog analytics.
      * 3 modes of anonymity are supported, governed by this.anonymity
      * - Anonymity.Disabled means *no data* is passed to posthog
-     * - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog
-     * - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being passed
-     *   to Posthog
+     * - Anonymity.Anonymous means no identifier is passed to posthog
+     * - Anonymity.Pseudonymous means an analytics ID stored in account_data and shared between devices
+     *   is passed to posthog.
      *
      * To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
      *
      * To pass an event to Posthog:
      *
-     * 1. Declare a type for the event, extending IAnonymousEvent, IPseudonymousEvent or IRoomEvent.
+     * 1. Declare a type for the event, extending IAnonymousEvent or IPseudonymousEvent.
      * 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is
      *    Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled.
      */
@@ -141,6 +133,7 @@ export class PosthogAnalytics {
     private enabled = false;
     private static _instance = null;
     private platformSuperProperties = {};
+    private static ANALYTICS_ID_EVENT_TYPE = "im.vector.web.analytics_id";
 
     public static get instance(): PosthogAnalytics {
         if (!this._instance) {
@@ -274,9 +267,32 @@ export class PosthogAnalytics {
         this.anonymity = anonymity;
     }
 
-    public async identifyUser(userId: string): Promise<void> {
+    private static getRandomAnalyticsId(): string {
+        return [...crypto.getRandomValues(new Uint8Array(16))].map((c) => c.toString(16)).join('');
+    }
+
+    public async identifyUser(client: MatrixClient, analyticsIdGenerator: () => string): Promise<void> {
         if (this.anonymity == Anonymity.Pseudonymous) {
-            this.posthog.identify(await hashHex(userId));
+            // Check the user's account_data for an analytics ID to use. Storing the ID in account_data allows
+            // different devices to send the same ID.
+            try {
+                const accountData = await client.getAccountDataFromServer(PosthogAnalytics.ANALYTICS_ID_EVENT_TYPE);
+                let analyticsID = accountData?.id;
+                if (!analyticsID) {
+                    // Couldn't retrieve an analytics ID from user settings, so create one and set it on the server.
+                    // Note there's a race condition here - if two devices do these steps at the same time, last write
+                    // wins, and the first writer will send tracking with an ID that doesn't match the one on the server
+                    // until the next time account data is refreshed and this function is called (most likely on next
+                    // page load). This will happen pretty infrequently, so we can tolerate the possibility.
+                    analyticsID = analyticsIdGenerator();
+                    await client.setAccountData("im.vector.web.analytics_id", { id: analyticsID });
+                }
+                this.posthog.identify(analyticsID);
+            } catch (e) {
+                // The above could fail due to network requests, but not essential to starting the application,
+                // so swallow it.
+                console.log("Unable to identify user for tracking" + e.toString());
+            }
         }
     }
 
@@ -307,18 +323,6 @@ export class PosthogAnalytics {
         await this.capture(eventName, properties);
     }
 
-    public async trackRoomEvent<E extends IRoomEvent>(
-        eventName: E["eventName"],
-        roomId: string,
-        properties: Omit<E["properties"], "roomId">,
-    ): Promise<void> {
-        const updatedProperties = {
-            ...properties,
-            hashedRoomId: roomId ? await hashHex(roomId) : null,
-        };
-        await this.trackPseudonymousEvent(eventName, updatedProperties);
-    }
-
     public async trackPageView(durationMs: number): Promise<void> {
         const hash = window.location.hash;
 
@@ -349,7 +353,7 @@ export class PosthogAnalytics {
         // Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
         this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
         if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
-            await this.identifyUser(userId);
+            await this.identifyUser(MatrixClientPeg.get(), PosthogAnalytics.getRandomAnalyticsId);
         }
     }
 }
diff --git a/src/Resend.ts b/src/Resend.ts
index 38b84a28e0..be9fb9550b 100644
--- a/src/Resend.ts
+++ b/src/Resend.ts
@@ -48,11 +48,6 @@ export default class Resend {
             // XXX: temporary logging to try to diagnose
             // https://github.com/vector-im/element-web/issues/3148
             console.log('Resend got send failure: ' + err.name + '(' + err + ')');
-
-            dis.dispatch({
-                action: 'message_send_failed',
-                event: event,
-            });
         });
     }
 
diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
index 2748fda35a..ac7875b920 100644
--- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
+++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
@@ -26,10 +26,9 @@ import { SettingLevel } from "../../../../settings/SettingLevel";
 import Field from '../../../../components/views/elements/Field';
 import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
 import DialogButtons from "../../../../components/views/elements/DialogButtons";
+import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
 
-interface IProps {
-    onFinished: (confirmed: boolean) => void;
-}
+interface IProps extends IDialogProps {}
 
 interface IState {
     eventIndexSize: number;
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index 332b6cd318..d65f8e3a10 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -322,10 +322,16 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
 
         const menuClasses = classNames({
             'mx_ContextualMenu': true,
-            'mx_ContextualMenu_left': !hasChevron && position.left,
-            'mx_ContextualMenu_right': !hasChevron && position.right,
-            'mx_ContextualMenu_top': !hasChevron && position.top,
-            'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
+            /**
+             * In some cases we may get the number of 0, which still means that we're supposed to properly
+             * add the specific position class, but as it was falsy things didn't work as intended.
+             * In addition, defensively check for counter cases where we may get more than one value,
+             * even if we shouldn't.
+             */
+            'mx_ContextualMenu_left': !hasChevron && position.left !== undefined && !position.right,
+            'mx_ContextualMenu_right': !hasChevron && position.right !== undefined && !position.left,
+            'mx_ContextualMenu_top': !hasChevron && position.top !== undefined && !position.bottom,
+            'mx_ContextualMenu_bottom': !hasChevron && position.bottom !== undefined && !position.top,
             'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left,
             'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
             'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
@@ -404,17 +410,27 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
     }
 }
 
+export type ToRightOf = {
+    left: number;
+    top: number;
+    chevronOffset: number;
+};
+
 // Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
-export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">, chevronOffset = 12) => {
+export const toRightOf = (elementRect: Pick<DOMRect, "right" | "top" | "height">, chevronOffset = 12): ToRightOf => {
     const left = elementRect.right + window.pageXOffset + 3;
     let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
     top -= chevronOffset + 8; // where 8 is half the height of the chevron
     return { left, top, chevronOffset };
 };
 
+export type AboveLeftOf = IPosition & {
+    chevronFace: ChevronFace;
+};
+
 // Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
 // and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
-export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
+export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0): AboveLeftOf => {
     const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
 
     const buttonRight = elementRect.right + window.pageXOffset;
diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.tsx
similarity index 74%
rename from src/components/structures/EmbeddedPage.js
rename to src/components/structures/EmbeddedPage.tsx
index 037a0eba2a..ec37eab254 100644
--- a/src/components/structures/EmbeddedPage.js
+++ b/src/components/structures/EmbeddedPage.tsx
@@ -17,7 +17,6 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import request from 'browser-request';
 import { _t } from '../../languageHandler';
 import sanitizeHtml from 'sanitize-html';
@@ -26,38 +25,43 @@ import { MatrixClientPeg } from '../../MatrixClientPeg';
 import classnames from 'classnames';
 import MatrixClientContext from "../../contexts/MatrixClientContext";
 import AutoHideScrollbar from "./AutoHideScrollbar";
+import { ActionPayload } from "../../dispatcher/payloads";
 
-export default class EmbeddedPage extends React.PureComponent {
-    static propTypes = {
-        // URL to request embedded page content from
-        url: PropTypes.string,
-        // Class name prefix to apply for a given instance
-        className: PropTypes.string,
-        // Whether to wrap the page in a scrollbar
-        scrollbar: PropTypes.bool,
-        // Map of keys to replace with values, e.g {$placeholder: "value"}
-        replaceMap: PropTypes.object,
-    };
+interface IProps {
+    // URL to request embedded page content from
+    url?: string;
+    // Class name prefix to apply for a given instance
+    className?: string;
+    // Whether to wrap the page in a scrollbar
+    scrollbar?: boolean;
+    // Map of keys to replace with values, e.g {$placeholder: "value"}
+    replaceMap?: Map<string, string>;
+}
 
-    static contextType = MatrixClientContext;
+interface IState {
+    page: string;
+}
 
-    constructor(props, context) {
+export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
+    public static contextType = MatrixClientContext;
+    private unmounted = false;
+    private dispatcherRef: string = null;
+
+    constructor(props: IProps, context: typeof MatrixClientContext) {
         super(props, context);
 
-        this._dispatcherRef = null;
-
         this.state = {
             page: '',
         };
     }
 
-    translate(s) {
+    protected translate(s: string): string {
         // default implementation - skins may wish to extend this
         return sanitizeHtml(_t(s));
     }
 
-    componentDidMount() {
-        this._unmounted = false;
+    public componentDidMount(): void {
+        this.unmounted = false;
 
         if (!this.props.url) {
             return;
@@ -70,7 +74,7 @@ export default class EmbeddedPage extends React.PureComponent {
         request(
             { method: "GET", url: this.props.url },
             (err, response, body) => {
-                if (this._unmounted) {
+                if (this.unmounted) {
                     return;
                 }
 
@@ -92,22 +96,22 @@ export default class EmbeddedPage extends React.PureComponent {
             },
         );
 
-        this._dispatcherRef = dis.register(this.onAction);
+        this.dispatcherRef = dis.register(this.onAction);
     }
 
-    componentWillUnmount() {
-        this._unmounted = true;
-        if (this._dispatcherRef !== null) dis.unregister(this._dispatcherRef);
+    public componentWillUnmount(): void {
+        this.unmounted = true;
+        if (this.dispatcherRef !== null) dis.unregister(this.dispatcherRef);
     }
 
-    onAction = (payload) => {
+    private onAction = (payload: ActionPayload): void => {
         // HACK: Workaround for the context's MatrixClient not being set up at render time.
         if (payload.action === 'client_started') {
             this.forceUpdate();
         }
     };
 
-    render() {
+    public render(): JSX.Element {
         // HACK: Workaround for the context's MatrixClient not updating.
         const client = this.context || MatrixClientPeg.get();
         const isGuest = client ? client.isGuest() : true;
diff --git a/src/components/structures/GenericErrorPage.js b/src/components/structures/GenericErrorPage.tsx
similarity index 84%
rename from src/components/structures/GenericErrorPage.js
rename to src/components/structures/GenericErrorPage.tsx
index 017d365273..d124c7111a 100644
--- a/src/components/structures/GenericErrorPage.js
+++ b/src/components/structures/GenericErrorPage.tsx
@@ -15,16 +15,15 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import { replaceableComponent } from "../../utils/replaceableComponent";
 
-@replaceableComponent("structures.GenericErrorPage")
-export default class GenericErrorPage extends React.PureComponent {
-    static propTypes = {
-        title: PropTypes.object.isRequired, // jsx for title
-        message: PropTypes.object.isRequired, // jsx to display
-    };
+interface IProps {
+    title: React.ReactNode;
+    message: React.ReactNode;
+}
 
+@replaceableComponent("structures.GenericErrorPage")
+export default class GenericErrorPage extends React.PureComponent<IProps> {
     render() {
         return <div className='mx_GenericErrorPage'>
             <div className='mx_GenericErrorPage_box'>
diff --git a/src/components/structures/GroupFilterPanel.tsx b/src/components/structures/GroupFilterPanel.tsx
index 3e7c6e9b17..b6d05efa87 100644
--- a/src/components/structures/GroupFilterPanel.tsx
+++ b/src/components/structures/GroupFilterPanel.tsx
@@ -146,19 +146,13 @@ class GroupFilterPanel extends React.Component<IGroupFilterPanelProps, IGroupFil
             mx_GroupFilterPanel_items_selected: itemsSelected,
         });
 
-        let betaDot;
-        if (SettingsStore.getBetaInfo("feature_spaces") && !localStorage.getItem("mx_seenSpacesBeta")) {
-            betaDot = <div className="mx_BetaDot" />;
-        }
-
         let createButton = (
             <ActionButton
                 tooltip
                 label={_t("Communities")}
                 action="toggle_my_groups"
-                className="mx_TagTile mx_TagTile_plus">
-                { betaDot }
-            </ActionButton>
+                className="mx_TagTile mx_TagTile_plus"
+            />
         );
 
         if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.tsx
similarity index 54%
rename from src/components/structures/IndicatorScrollbar.js
rename to src/components/structures/IndicatorScrollbar.tsx
index 3e1940955b..85de659481 100644
--- a/src/components/structures/IndicatorScrollbar.js
+++ b/src/components/structures/IndicatorScrollbar.tsx
@@ -14,34 +14,39 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from "react";
-import PropTypes from "prop-types";
+import React, { createRef } from "react";
 import AutoHideScrollbar from "./AutoHideScrollbar";
 import { replaceableComponent } from "../../utils/replaceableComponent";
 
+interface IProps {
+    // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
+    // and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
+    // by the parent element.
+    trackHorizontalOverflow?: boolean;
+
+    // If true, when the user tries to use their mouse wheel in the component it will
+    // scroll horizontally rather than vertically. This should only be used on components
+    // with no vertical scroll opportunity.
+    verticalScrollsHorizontally?: boolean;
+
+    children: React.ReactNode;
+    className: string;
+}
+
+interface IState {
+    leftIndicatorOffset: number | string;
+    rightIndicatorOffset: number | string;
+}
+
 @replaceableComponent("structures.IndicatorScrollbar")
-export default class IndicatorScrollbar extends React.Component {
-    static propTypes = {
-        // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
-        // and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
-        // by the parent element.
-        trackHorizontalOverflow: PropTypes.bool,
+export default class IndicatorScrollbar extends React.Component<IProps, IState> {
+    private autoHideScrollbar = createRef<AutoHideScrollbar>();
+    private scrollElement: HTMLDivElement;
+    private likelyTrackpadUser: boolean = null;
+    private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
 
-        // If true, when the user tries to use their mouse wheel in the component it will
-        // scroll horizontally rather than vertically. This should only be used on components
-        // with no vertical scroll opportunity.
-        verticalScrollsHorizontally: PropTypes.bool,
-    };
-
-    constructor(props) {
+    constructor(props: IProps) {
         super(props);
-        this._collectScroller = this._collectScroller.bind(this);
-        this._collectScrollerComponent = this._collectScrollerComponent.bind(this);
-        this.checkOverflow = this.checkOverflow.bind(this);
-        this._scrollElement = null;
-        this._autoHideScrollbar = null;
-        this._likelyTrackpadUser = null;
-        this._checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
 
         this.state = {
             leftIndicatorOffset: 0,
@@ -49,30 +54,19 @@ export default class IndicatorScrollbar extends React.Component {
         };
     }
 
-    moveToOrigin() {
-        if (!this._scrollElement) return;
-
-        this._scrollElement.scrollLeft = 0;
-        this._scrollElement.scrollTop = 0;
-    }
-
-    _collectScroller(scroller) {
-        if (scroller && !this._scrollElement) {
-            this._scrollElement = scroller;
+    private collectScroller = (scroller: HTMLDivElement): void => {
+        if (scroller && !this.scrollElement) {
+            this.scrollElement = scroller;
             // Using the passive option to not block the main thread
             // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
-            this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
+            this.scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
             this.checkOverflow();
         }
-    }
+    };
 
-    _collectScrollerComponent(autoHideScrollbar) {
-        this._autoHideScrollbar = autoHideScrollbar;
-    }
-
-    componentDidUpdate(prevProps) {
-        const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
-        const curLen = this.props.children && this.props.children.length || 0;
+    public componentDidUpdate(prevProps: IProps): void {
+        const prevLen = React.Children.count(prevProps.children);
+        const curLen = React.Children.count(this.props.children);
         // check overflow only if amount of children changes.
         // if we don't guard here, we end up with an infinite
         // render > componentDidUpdate > checkOverflow > setState > render loop
@@ -81,62 +75,58 @@ export default class IndicatorScrollbar extends React.Component {
         }
     }
 
-    componentDidMount() {
+    public componentDidMount(): void {
         this.checkOverflow();
     }
 
-    checkOverflow() {
-        const hasTopOverflow = this._scrollElement.scrollTop > 0;
-        const hasBottomOverflow = this._scrollElement.scrollHeight >
-            (this._scrollElement.scrollTop + this._scrollElement.clientHeight);
-        const hasLeftOverflow = this._scrollElement.scrollLeft > 0;
-        const hasRightOverflow = this._scrollElement.scrollWidth >
-            (this._scrollElement.scrollLeft + this._scrollElement.clientWidth);
+    private checkOverflow = (): void => {
+        const hasTopOverflow = this.scrollElement.scrollTop > 0;
+        const hasBottomOverflow = this.scrollElement.scrollHeight >
+            (this.scrollElement.scrollTop + this.scrollElement.clientHeight);
+        const hasLeftOverflow = this.scrollElement.scrollLeft > 0;
+        const hasRightOverflow = this.scrollElement.scrollWidth >
+            (this.scrollElement.scrollLeft + this.scrollElement.clientWidth);
 
         if (hasTopOverflow) {
-            this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
+            this.scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
         } else {
-            this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
+            this.scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
         }
         if (hasBottomOverflow) {
-            this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
+            this.scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
         } else {
-            this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
+            this.scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
         }
         if (hasLeftOverflow) {
-            this._scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
+            this.scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
         } else {
-            this._scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
+            this.scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
         }
         if (hasRightOverflow) {
-            this._scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
+            this.scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
         } else {
-            this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
+            this.scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
         }
 
         if (this.props.trackHorizontalOverflow) {
             this.setState({
                 // Offset from absolute position of the container
-                leftIndicatorOffset: hasLeftOverflow ? `${this._scrollElement.scrollLeft}px` : '0',
+                leftIndicatorOffset: hasLeftOverflow ? `${this.scrollElement.scrollLeft}px` : '0',
 
                 // Negative because we're coming from the right
-                rightIndicatorOffset: hasRightOverflow ? `-${this._scrollElement.scrollLeft}px` : '0',
+                rightIndicatorOffset: hasRightOverflow ? `-${this.scrollElement.scrollLeft}px` : '0',
             });
         }
-    }
+    };
 
-    getScrollTop() {
-        return this._autoHideScrollbar.getScrollTop();
-    }
-
-    componentWillUnmount() {
-        if (this._scrollElement) {
-            this._scrollElement.removeEventListener("scroll", this.checkOverflow);
+    public componentWillUnmount(): void {
+        if (this.scrollElement) {
+            this.scrollElement.removeEventListener("scroll", this.checkOverflow);
         }
     }
 
-    onMouseWheel = (e) => {
-        if (this.props.verticalScrollsHorizontally && this._scrollElement) {
+    private onMouseWheel = (e: React.WheelEvent): void => {
+        if (this.props.verticalScrollsHorizontally && this.scrollElement) {
             // xyThreshold is the amount of horizontal motion required for the component to
             // ignore the vertical delta in a scroll. Used to stop trackpads from acting in
             // strange ways. Should be positive.
@@ -150,19 +140,19 @@ export default class IndicatorScrollbar extends React.Component {
             // for at least the next 1 minute.
             const now = new Date().getTime();
             if (Math.abs(e.deltaX) > 0) {
-                this._likelyTrackpadUser = true;
-                this._checkAgainForTrackpad = now + (1 * 60 * 1000);
+                this.likelyTrackpadUser = true;
+                this.checkAgainForTrackpad = now + (1 * 60 * 1000);
             } else {
                 // if we haven't seen any horizontal scrolling for a while, assume
                 // the user might have plugged in a mousewheel
-                if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) {
-                    this._likelyTrackpadUser = false;
+                if (this.likelyTrackpadUser && now >= this.checkAgainForTrackpad) {
+                    this.likelyTrackpadUser = false;
                 }
             }
 
             // don't mess with the horizontal scroll for trackpad users
             // See https://github.com/vector-im/element-web/issues/10005
-            if (this._likelyTrackpadUser) {
+            if (this.likelyTrackpadUser) {
                 return;
             }
 
@@ -178,13 +168,13 @@ export default class IndicatorScrollbar extends React.Component {
 
                 // noinspection JSSuspiciousNameCombination
                 const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY;
-                this._scrollElement.scrollLeft += val * yRetention;
+                this.scrollElement.scrollLeft += val * yRetention;
             }
         }
     };
 
-    render() {
-        // eslint-disable-next-line no-unused-vars
+    public render(): JSX.Element {
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
         const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
 
         const leftIndicatorStyle = { left: this.state.leftIndicatorOffset };
@@ -195,8 +185,8 @@ export default class IndicatorScrollbar extends React.Component {
             ? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
 
         return (<AutoHideScrollbar
-            ref={this._collectScrollerComponent}
-            wrappedRef={this._collectScroller}
+            ref={this.autoHideScrollbar}
+            wrappedRef={this.collectScroller}
             onWheel={this.onMouseWheel}
             {...otherProps}
         >
diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx
index 331e428355..6b91acb5f8 100644
--- a/src/components/structures/LeftPanelWidget.tsx
+++ b/src/components/structures/LeftPanelWidget.tsx
@@ -76,7 +76,6 @@ const LeftPanelWidget: React.FC = () => {
             <AppTile
                 app={app}
                 fullWidth
-                show
                 showMenubar={false}
                 userWidget
                 userId={cli.getUserId()}
diff --git a/src/components/structures/LegacyCommunityPreview.tsx b/src/components/structures/LegacyCommunityPreview.tsx
new file mode 100644
index 0000000000..32bafeb3fa
--- /dev/null
+++ b/src/components/structures/LegacyCommunityPreview.tsx
@@ -0,0 +1,115 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useContext } from "react";
+
+import MatrixClientContext from "../../contexts/MatrixClientContext";
+import { _t } from "../../languageHandler";
+import AccessibleButton from "../views/elements/AccessibleButton";
+import ErrorBoundary from "../views/elements/ErrorBoundary";
+import { IGroupSummary } from "../views/dialogs/CreateSpaceFromCommunityDialog";
+import { useAsyncMemo } from "../../hooks/useAsyncMemo";
+import Spinner from "../views/elements/Spinner";
+import GroupAvatar from "../views/avatars/GroupAvatar";
+import { linkifyElement } from "../../HtmlUtils";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import { Action } from "../../dispatcher/actions";
+import { UserTab } from "../views/dialogs/UserSettingsDialog";
+
+interface IProps {
+    groupId: string;
+}
+
+const onSwapClick = () => {
+    defaultDispatcher.dispatch({
+        action: Action.ViewUserSettings,
+        initialTabId: UserTab.Preferences,
+    });
+};
+
+// XXX: temporary community migration component, reuses SpaceRoomView & SpacePreview classes for simplicity
+const LegacyCommunityPreview = ({ groupId }: IProps) => {
+    const cli = useContext(MatrixClientContext);
+
+    const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [cli, groupId]);
+
+    if (!groupSummary) {
+        return <main className="mx_SpaceRoomView">
+            <div className="mx_MainSplit">
+                <div className="mx_SpaceRoomView_preview">
+                    <Spinner />
+                </div>
+            </div>
+        </main>;
+    }
+
+    let visibilitySection: JSX.Element;
+    if (groupSummary.profile.is_public) {
+        visibilitySection = <span className="mx_SpaceRoomView_info_public">
+            { _t("Public community") }
+        </span>;
+    } else {
+        visibilitySection = <span className="mx_SpaceRoomView_info_private">
+            { _t("Private community") }
+        </span>;
+    }
+
+    return <main className="mx_SpaceRoomView">
+        <ErrorBoundary>
+            <div className="mx_MainSplit">
+                <div className="mx_SpaceRoomView_preview">
+                    <GroupAvatar
+                        groupId={groupId}
+                        groupName={groupSummary.profile.name}
+                        groupAvatarUrl={groupSummary.profile.avatar_url}
+                        height={80}
+                        width={80}
+                        resizeMethod='crop'
+                    />
+                    <h1 className="mx_SpaceRoomView_preview_name">
+                        { groupSummary.profile.name }
+                    </h1>
+                    <div className="mx_SpaceRoomView_info">
+                        { visibilitySection }
+                    </div>
+                    <div className="mx_SpaceRoomView_preview_topic" ref={e => e && linkifyElement(e)}>
+                        { groupSummary.profile.short_description }
+                    </div>
+                    <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
+                        { groupSummary.user?.membership === "join"
+                            ? _t("To view %(communityName)s, swap to communities in your <a>preferences</a>", {
+                                communityName: groupSummary.profile.name,
+                            }, {
+                                a: sub => (
+                                    <AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
+                                ),
+                            })
+                            : _t("To join %(communityName)s, swap to communities in your <a>preferences</a>", {
+                                communityName: groupSummary.profile.name,
+                            }, {
+                                a: sub => (
+                                    <AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
+                                ),
+                            })
+                        }
+                    </div>
+                </div>
+            </div>
+        </ErrorBoundary>
+    </main>;
+};
+
+export default LegacyCommunityPreview;
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index bbe0e42a0a..84e0b446f5 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -69,6 +69,7 @@ import classNames from 'classnames';
 import GroupFilterPanel from './GroupFilterPanel';
 import CustomRoomTagPanel from './CustomRoomTagPanel';
 import { mediaFromMxc } from "../../customisations/Media";
+import LegacyCommunityPreview from "./LegacyCommunityPreview";
 
 // We need to fetch each pinned message individually (if we don't already have it)
 // so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -629,11 +630,15 @@ class LoggedInView extends React.Component<IProps, IState> {
                 pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
                 break;
             case PageTypes.GroupView:
-                pageElement = <GroupView
-                    groupId={this.props.currentGroupId}
-                    isNew={this.props.currentGroupIsNew}
-                    resizeNotifier={this.props.resizeNotifier}
-                />;
+                if (SpaceStore.spacesEnabled) {
+                    pageElement = <LegacyCommunityPreview groupId={this.props.currentGroupId} />;
+                } else {
+                    pageElement = <GroupView
+                        groupId={this.props.currentGroupId}
+                        isNew={this.props.currentGroupIsNew}
+                        resizeNotifier={this.props.resizeNotifier}
+                    />;
+                }
                 break;
         }
 
diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.tsx
similarity index 68%
rename from src/components/structures/MainSplit.js
rename to src/components/structures/MainSplit.tsx
index 69d3bd0b51..af7645767d 100644
--- a/src/components/structures/MainSplit.js
+++ b/src/components/structures/MainSplit.tsx
@@ -16,25 +16,35 @@ limitations under the License.
 */
 
 import React from 'react';
-import { Resizable } from 're-resizable';
+import { NumberSize, Resizable } from 're-resizable';
 import { replaceableComponent } from "../../utils/replaceableComponent";
+import ResizeNotifier from "../../utils/ResizeNotifier";
+import { Direction } from "re-resizable/lib/resizer";
+
+interface IProps {
+    resizeNotifier: ResizeNotifier;
+    collapsedRhs?: boolean;
+    panel?: JSX.Element;
+}
 
 @replaceableComponent("structures.MainSplit")
-export default class MainSplit extends React.Component {
-    _onResizeStart = () => {
+export default class MainSplit extends React.Component<IProps> {
+    private onResizeStart = (): void => {
         this.props.resizeNotifier.startResizing();
     };
 
-    _onResize = () => {
+    private onResize = (): void => {
         this.props.resizeNotifier.notifyRightHandleResized();
     };
 
-    _onResizeStop = (event, direction, refToElement, delta) => {
+    private onResizeStop = (
+        event: MouseEvent | TouchEvent, direction: Direction, elementRef: HTMLElement, delta: NumberSize,
+    ): void => {
         this.props.resizeNotifier.stopResizing();
-        window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
+        window.localStorage.setItem("mx_rhs_size", (this.loadSidePanelSize().width + delta.width).toString());
     };
 
-    _loadSidePanelSize() {
+    private loadSidePanelSize(): {height: string | number, width: number} {
         let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
 
         if (isNaN(rhsSize)) {
@@ -47,7 +57,7 @@ export default class MainSplit extends React.Component {
         };
     }
 
-    render() {
+    public render(): JSX.Element {
         const bodyView = React.Children.only(this.props.children);
         const panelView = this.props.panel;
 
@@ -56,7 +66,7 @@ export default class MainSplit extends React.Component {
         let children;
         if (hasResizer) {
             children = <Resizable
-                defaultSize={this._loadSidePanelSize()}
+                defaultSize={this.loadSidePanelSize()}
                 minWidth={264}
                 maxWidth="50%"
                 enable={{
@@ -69,9 +79,9 @@ export default class MainSplit extends React.Component {
                     bottomLeft: false,
                     topLeft: false,
                 }}
-                onResizeStart={this._onResizeStart}
-                onResize={this._onResize}
-                onResizeStop={this._onResizeStop}
+                onResizeStart={this.onResizeStart}
+                onResize={this.onResize}
+                onResizeStop={this.onResizeStop}
                 className="mx_RightPanel_ResizeWrapper"
                 handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
             >
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 531dc9fbe9..2ab68998c3 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -143,7 +143,7 @@ export enum Views {
     SOFT_LOGOUT,
 }
 
-const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas"];
+const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];
 
 // Actions that are redirected through the onboarding process prior to being
 // re-dispatched. NOTE: some actions are non-trivial and would require
@@ -1800,11 +1800,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                 subAction: params.action,
             });
         } else if (screen.indexOf('group/') === 0) {
-            if (SpaceStore.spacesEnabled) {
-                dis.dispatch({ action: "view_home_page" });
-                return;
-            }
-
             const groupId = screen.substring(6);
 
             // TODO: Check valid group ID
@@ -1897,15 +1892,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
 
     onSendEvent(roomId: string, event: MatrixEvent) {
         const cli = MatrixClientPeg.get();
-        if (!cli) {
-            dis.dispatch({ action: 'message_send_failed' });
-            return;
-        }
+        if (!cli) return;
 
         cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
             dis.dispatch({ action: 'message_sent' });
-        }, (err) => {
-            dis.dispatch({ action: 'message_send_failed' });
         });
     }
 
diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 589947af73..74f281405c 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -448,7 +448,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
         // Always show highlighted event
         if (this.props.highlightedEventId === mxEv.getId()) return true;
 
-        if (mxEv.replyInThread
+        // Checking if the message has a "parentEventId" as we do not
+        // want to hide the root event of the thread
+        if (mxEv.replyInThread && mxEv.parentEventId
                 && this.props.hideThreadedMessages
                 && SettingsStore.getValue("feature_thread")) {
             return false;
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index dab18c4161..2239325f1b 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -25,7 +25,6 @@ import AccessibleButton from '../views/elements/AccessibleButton';
 import MatrixClientContext from "../../contexts/MatrixClientContext";
 import AutoHideScrollbar from "./AutoHideScrollbar";
 import { replaceableComponent } from "../../utils/replaceableComponent";
-import BetaCard from "../views/beta/BetaCard";
 
 @replaceableComponent("structures.MyGroups")
 export default class MyGroups extends React.Component {
@@ -138,7 +137,6 @@ export default class MyGroups extends React.Component {
                     </div>
                 </div>*/ }
             </div>
-            <BetaCard featureId="feature_spaces" title={_t("Communities are changing to Spaces")} />
             <div className="mx_MyGroups_content">
                 { contentHeader }
                 { content }
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.tsx
similarity index 71%
rename from src/components/structures/RoomStatusBar.js
rename to src/components/structures/RoomStatusBar.tsx
index 8b10c54cba..82f68bc435 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.tsx
@@ -15,95 +15,110 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import { _t, _td } from '../../languageHandler';
-import { MatrixClientPeg } from '../../MatrixClientPeg';
 import Resend from '../../Resend';
 import dis from '../../dispatcher/dispatcher';
 import { messageForResourceLimitError } from '../../utils/ErrorUtils';
 import { Action } from "../../dispatcher/actions";
 import { replaceableComponent } from "../../utils/replaceableComponent";
-import { EventStatus } from "matrix-js-sdk/src/models/event";
+import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
 import NotificationBadge from "../views/rooms/NotificationBadge";
 import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
 import AccessibleButton from "../views/elements/AccessibleButton";
 import InlineSpinner from "../views/elements/InlineSpinner";
+import { SyncState } from "matrix-js-sdk/src/sync.api";
+import { ISyncStateData } from "matrix-js-sdk/src/sync";
+import { Room } from "matrix-js-sdk/src/models/room";
+import MatrixClientContext from "../../contexts/MatrixClientContext";
 
 const STATUS_BAR_HIDDEN = 0;
 const STATUS_BAR_EXPANDED = 1;
 const STATUS_BAR_EXPANDED_LARGE = 2;
 
-export function getUnsentMessages(room) {
+export function getUnsentMessages(room: Room): MatrixEvent[] {
     if (!room) { return []; }
     return room.getPendingEvents().filter(function(ev) {
         return ev.status === EventStatus.NOT_SENT;
     });
 }
 
+interface IProps {
+    // the room this statusbar is representing.
+    room: Room;
+
+    // true if the room is being peeked at. This affects components that shouldn't
+    // logically be shown when peeking, such as a prompt to invite people to a room.
+    isPeeking?: boolean;
+    // callback for when the user clicks on the 'resend all' button in the
+    // 'unsent messages' bar
+    onResendAllClick?: () => void;
+
+    // callback for when the user clicks on the 'cancel all' button in the
+    // 'unsent messages' bar
+    onCancelAllClick?: () => void;
+
+    // callback for when the user clicks on the 'invite others' button in the
+    // 'you are alone' bar
+    onInviteClick?: () => void;
+
+    // callback for when we do something that changes the size of the
+    // status bar. This is used to trigger a re-layout in the parent
+    // component.
+    onResize?: () => void;
+
+    // callback for when the status bar can be hidden from view, as it is
+    // not displaying anything
+    onHidden?: () => void;
+
+    // callback for when the status bar is displaying something and should
+    // be visible
+    onVisible?: () => void;
+}
+
+interface IState {
+    syncState: SyncState;
+    syncStateData: ISyncStateData;
+    unsentMessages: MatrixEvent[];
+    isResending: boolean;
+}
+
 @replaceableComponent("structures.RoomStatusBar")
-export default class RoomStatusBar extends React.PureComponent {
-    static propTypes = {
-        // the room this statusbar is representing.
-        room: PropTypes.object.isRequired,
+export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
+    public static contextType = MatrixClientContext;
 
-        // true if the room is being peeked at. This affects components that shouldn't
-        // logically be shown when peeking, such as a prompt to invite people to a room.
-        isPeeking: PropTypes.bool,
+    constructor(props: IProps, context: typeof MatrixClientContext) {
+        super(props, context);
 
-        // callback for when the user clicks on the 'resend all' button in the
-        // 'unsent messages' bar
-        onResendAllClick: PropTypes.func,
-
-        // callback for when the user clicks on the 'cancel all' button in the
-        // 'unsent messages' bar
-        onCancelAllClick: PropTypes.func,
-
-        // callback for when the user clicks on the 'invite others' button in the
-        // 'you are alone' bar
-        onInviteClick: PropTypes.func,
-
-        // callback for when we do something that changes the size of the
-        // status bar. This is used to trigger a re-layout in the parent
-        // component.
-        onResize: PropTypes.func,
-
-        // callback for when the status bar can be hidden from view, as it is
-        // not displaying anything
-        onHidden: PropTypes.func,
-
-        // callback for when the status bar is displaying something and should
-        // be visible
-        onVisible: PropTypes.func,
-    };
-
-    state = {
-        syncState: MatrixClientPeg.get().getSyncState(),
-        syncStateData: MatrixClientPeg.get().getSyncStateData(),
-        unsentMessages: getUnsentMessages(this.props.room),
-        isResending: false,
-    };
-
-    componentDidMount() {
-        MatrixClientPeg.get().on("sync", this.onSyncStateChange);
-        MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
-
-        this._checkSize();
+        this.state = {
+            syncState: this.context.getSyncState(),
+            syncStateData: this.context.getSyncStateData(),
+            unsentMessages: getUnsentMessages(this.props.room),
+            isResending: false,
+        };
     }
 
-    componentDidUpdate() {
-        this._checkSize();
+    public componentDidMount(): void {
+        const client = this.context;
+        client.on("sync", this.onSyncStateChange);
+        client.on("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
+
+        this.checkSize();
     }
 
-    componentWillUnmount() {
+    public componentDidUpdate(): void {
+        this.checkSize();
+    }
+
+    public componentWillUnmount(): void {
         // we may have entirely lost our client as we're logging out before clicking login on the guest bar...
-        const client = MatrixClientPeg.get();
+        const client = this.context;
         if (client) {
             client.removeListener("sync", this.onSyncStateChange);
-            client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
+            client.removeListener("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
         }
     }
 
-    onSyncStateChange = (state, prevState, data) => {
+    private onSyncStateChange = (state: SyncState, prevState: SyncState, data: ISyncStateData): void => {
         if (state === "SYNCING" && prevState === "SYNCING") {
             return;
         }
@@ -113,7 +128,7 @@ export default class RoomStatusBar extends React.PureComponent {
         });
     };
 
-    _onResendAllClick = () => {
+    private onResendAllClick = (): void => {
         Resend.resendUnsentEvents(this.props.room).then(() => {
             this.setState({ isResending: false });
         });
@@ -121,12 +136,12 @@ export default class RoomStatusBar extends React.PureComponent {
         dis.fire(Action.FocusSendMessageComposer);
     };
 
-    _onCancelAllClick = () => {
+    private onCancelAllClick = (): void => {
         Resend.cancelUnsentEvents(this.props.room);
         dis.fire(Action.FocusSendMessageComposer);
     };
 
-    _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
+    private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
         if (room.roomId !== this.props.room.roomId) return;
         const messages = getUnsentMessages(this.props.room);
         this.setState({
@@ -136,8 +151,8 @@ export default class RoomStatusBar extends React.PureComponent {
     };
 
     // Check whether current size is greater than 0, if yes call props.onVisible
-    _checkSize() {
-        if (this._getSize()) {
+    private checkSize(): void {
+        if (this.getSize()) {
             if (this.props.onVisible) this.props.onVisible();
         } else {
             if (this.props.onHidden) this.props.onHidden();
@@ -147,8 +162,8 @@ export default class RoomStatusBar extends React.PureComponent {
     // We don't need the actual height - just whether it is likely to have
     // changed - so we use '0' to indicate normal size, and other values to
     // indicate other sizes.
-    _getSize() {
-        if (this._shouldShowConnectionError()) {
+    private getSize(): number {
+        if (this.shouldShowConnectionError()) {
             return STATUS_BAR_EXPANDED;
         } else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
             return STATUS_BAR_EXPANDED_LARGE;
@@ -156,7 +171,7 @@ export default class RoomStatusBar extends React.PureComponent {
         return STATUS_BAR_HIDDEN;
     }
 
-    _shouldShowConnectionError() {
+    private shouldShowConnectionError(): boolean {
         // no conn bar trumps the "some not sent" msg since you can't resend without
         // a connection!
         // There's one situation in which we don't show this 'no connection' bar, and that's
@@ -164,12 +179,12 @@ export default class RoomStatusBar extends React.PureComponent {
         const errorIsMauError = Boolean(
             this.state.syncStateData &&
             this.state.syncStateData.error &&
-            this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED',
+            this.state.syncStateData.error.name === 'M_RESOURCE_LIMIT_EXCEEDED',
         );
         return this.state.syncState === "ERROR" && !errorIsMauError;
     }
 
-    _getUnsentMessageContent() {
+    private getUnsentMessageContent(): JSX.Element {
         const unsentMessages = this.state.unsentMessages;
 
         let title;
@@ -221,10 +236,10 @@ export default class RoomStatusBar extends React.PureComponent {
         }
 
         let buttonRow = <>
-            <AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
+            <AccessibleButton onClick={this.onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
                 { _t("Delete all") }
             </AccessibleButton>
-            <AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
+            <AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
                 { _t("Retry all") }
             </AccessibleButton>
         </>;
@@ -260,8 +275,8 @@ export default class RoomStatusBar extends React.PureComponent {
         </>;
     }
 
-    render() {
-        if (this._shouldShowConnectionError()) {
+    public render(): JSX.Element {
+        if (this.shouldShowConnectionError()) {
             return (
                 <div className="mx_RoomStatusBar">
                     <div role="alert">
@@ -287,7 +302,7 @@ export default class RoomStatusBar extends React.PureComponent {
         }
 
         if (this.state.unsentMessages.length > 0 || this.state.isResending) {
-            return this._getUnsentMessageContent();
+            return this.getUnsentMessageContent();
         }
 
         return null;
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.tsx
similarity index 62%
rename from src/components/structures/SearchBox.js
rename to src/components/structures/SearchBox.tsx
index 6d310662e3..82fe689022 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.tsx
@@ -16,7 +16,6 @@ limitations under the License.
 */
 
 import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
 import { Key } from '../../Keyboard';
 import dis from '../../dispatcher/dispatcher';
 import { throttle } from 'lodash';
@@ -24,106 +23,116 @@ import AccessibleButton from '../../components/views/elements/AccessibleButton';
 import classNames from 'classnames';
 import { replaceableComponent } from "../../utils/replaceableComponent";
 
+interface IProps {
+    onSearch?: (query: string) => void;
+    onCleared?: (source?: string) => void;
+    onKeyDown?: (ev: React.KeyboardEvent) => void;
+    onFocus?: (ev: React.FocusEvent) => void;
+    onBlur?: (ev: React.FocusEvent) => void;
+    className?: string;
+    placeholder: string;
+    blurredPlaceholder?: string;
+    autoFocus?: boolean;
+    initialValue?: string;
+    collapsed?: boolean;
+
+    // If true, the search box will focus and clear itself
+    // on room search focus action (it would be nicer to take
+    // this functionality out, but not obvious how that would work)
+    enableRoomSearchFocus?: boolean;
+}
+
+interface IState {
+    searchTerm: string;
+    blurred: boolean;
+}
+
 @replaceableComponent("structures.SearchBox")
-export default class SearchBox extends React.Component {
-    static propTypes = {
-        onSearch: PropTypes.func,
-        onCleared: PropTypes.func,
-        onKeyDown: PropTypes.func,
-        className: PropTypes.string,
-        placeholder: PropTypes.string.isRequired,
-        autoFocus: PropTypes.bool,
-        initialValue: PropTypes.string,
+export default class SearchBox extends React.Component<IProps, IState> {
+    private dispatcherRef: string;
+    private search = createRef<HTMLInputElement>();
 
-        // If true, the search box will focus and clear itself
-        // on room search focus action (it would be nicer to take
-        // this functionality out, but not obvious how that would work)
-        enableRoomSearchFocus: PropTypes.bool,
-    };
-
-    static defaultProps = {
+    static defaultProps: Partial<IProps> = {
         enableRoomSearchFocus: false,
     };
 
-    constructor(props) {
+    constructor(props: IProps) {
         super(props);
 
-        this._search = createRef();
-
         this.state = {
-            searchTerm: this.props.initialValue || "",
+            searchTerm: props.initialValue || "",
             blurred: true,
         };
     }
 
-    componentDidMount() {
+    public componentDidMount(): void {
         this.dispatcherRef = dis.register(this.onAction);
     }
 
-    componentWillUnmount() {
+    public componentWillUnmount(): void {
         dis.unregister(this.dispatcherRef);
     }
 
-    onAction = payload => {
+    private onAction = (payload): void => {
         if (!this.props.enableRoomSearchFocus) return;
 
         switch (payload.action) {
             case 'view_room':
-                if (this._search.current && payload.clear_search) {
-                    this._clearSearch();
+                if (this.search.current && payload.clear_search) {
+                    this.clearSearch();
                 }
                 break;
             case 'focus_room_filter':
-                if (this._search.current) {
-                    this._search.current.focus();
+                if (this.search.current) {
+                    this.search.current.focus();
                 }
                 break;
         }
     };
 
-    onChange = () => {
-        if (!this._search.current) return;
-        this.setState({ searchTerm: this._search.current.value });
+    private onChange = (): void => {
+        if (!this.search.current) return;
+        this.setState({ searchTerm: this.search.current.value });
         this.onSearch();
     };
 
-    onSearch = throttle(() => {
-        this.props.onSearch(this._search.current.value);
+    private onSearch = throttle((): void => {
+        this.props.onSearch(this.search.current.value);
     }, 200, { trailing: true, leading: true });
 
-    _onKeyDown = ev => {
+    private onKeyDown = (ev: React.KeyboardEvent): void => {
         switch (ev.key) {
             case Key.ESCAPE:
-                this._clearSearch("keyboard");
+                this.clearSearch("keyboard");
                 break;
         }
         if (this.props.onKeyDown) this.props.onKeyDown(ev);
     };
 
-    _onFocus = ev => {
+    private onFocus = (ev: React.FocusEvent): void => {
         this.setState({ blurred: false });
-        ev.target.select();
+        (ev.target as HTMLInputElement).select();
         if (this.props.onFocus) {
             this.props.onFocus(ev);
         }
     };
 
-    _onBlur = ev => {
+    private onBlur = (ev: React.FocusEvent): void => {
         this.setState({ blurred: true });
         if (this.props.onBlur) {
             this.props.onBlur(ev);
         }
     };
 
-    _clearSearch(source) {
-        this._search.current.value = "";
+    private clearSearch(source?: string): void {
+        this.search.current.value = "";
         this.onChange();
         if (this.props.onCleared) {
             this.props.onCleared(source);
         }
     }
 
-    render() {
+    public render(): JSX.Element {
         // check for collapsed here and
         // not at parent so we keep
         // searchTerm in our state
@@ -136,7 +145,7 @@ export default class SearchBox extends React.Component {
                 key="button"
                 tabIndex={-1}
                 className="mx_SearchBox_closeButton"
-                onClick={() => {this._clearSearch("button"); }}
+                onClick={() => {this.clearSearch("button"); }}
             />) : undefined;
 
         // show a shorter placeholder when blurred, if requested
@@ -151,13 +160,13 @@ export default class SearchBox extends React.Component {
                 <input
                     key="searchfield"
                     type="text"
-                    ref={this._search}
+                    ref={this.search}
                     className={"mx_textinput_icon mx_textinput_search " + className}
                     value={this.state.searchTerm}
-                    onFocus={this._onFocus}
+                    onFocus={this.onFocus}
                     onChange={this.onChange}
-                    onKeyDown={this._onKeyDown}
-                    onBlur={this._onBlur}
+                    onKeyDown={this.onKeyDown}
+                    onBlur={this.onBlur}
                     placeholder={placeholder}
                     autoComplete="off"
                     autoFocus={this.props.autoFocus}
diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx
index a0d4d9c42a..db16011917 100644
--- a/src/components/structures/SpaceHierarchy.tsx
+++ b/src/components/structures/SpaceHierarchy.tsx
@@ -57,12 +57,20 @@ import { Key } from "../../Keyboard";
 import { IState, RovingTabIndexProvider, useRovingTabIndex } from "../../accessibility/RovingTabIndex";
 import { getDisplayAliasForRoom } from "./RoomDirectory";
 import MatrixClientContext from "../../contexts/MatrixClientContext";
+import { useEventEmitterState } from "../../hooks/useEventEmitter";
+import { IOOBData } from "../../stores/ThreepidInviteStore";
 
 interface IProps {
     space: Room;
     initialText?: string;
     additionalButtons?: ReactNode;
-    showRoom(cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin?: boolean): void;
+    showRoom(
+        cli: MatrixClient,
+        hierarchy: RoomHierarchy,
+        roomId: string,
+        autoJoin?: boolean,
+        roomType?: RoomType,
+    ): void;
 }
 
 interface ITileProps {
@@ -71,7 +79,7 @@ interface ITileProps {
     selected?: boolean;
     numChildRooms?: number;
     hasPermissions?: boolean;
-    onViewRoomClick(autoJoin: boolean): void;
+    onViewRoomClick(autoJoin: boolean, roomType: RoomType): void;
     onToggleClick?(): void;
 }
 
@@ -87,7 +95,8 @@ const Tile: React.FC<ITileProps> = ({
 }) => {
     const cli = useContext(MatrixClientContext);
     const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
-    const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0]
+    const joinedRoomName = useEventEmitterState(joinedRoom, "Room.name", room => room?.name);
+    const name = joinedRoomName || room.name || room.canonical_alias || room.aliases?.[0]
         || (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
 
     const [showChildren, toggleShowChildren] = useStateToggle(true);
@@ -96,12 +105,12 @@ const Tile: React.FC<ITileProps> = ({
     const onPreviewClick = (ev: ButtonEvent) => {
         ev.preventDefault();
         ev.stopPropagation();
-        onViewRoomClick(false);
+        onViewRoomClick(false, room.room_type as RoomType);
     };
     const onJoinClick = (ev: ButtonEvent) => {
         ev.preventDefault();
         ev.stopPropagation();
-        onViewRoomClick(true);
+        onViewRoomClick(true, room.room_type as RoomType);
     };
 
     let button;
@@ -278,7 +287,13 @@ const Tile: React.FC<ITileProps> = ({
     </li>;
 };
 
-export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string, autoJoin = false) => {
+export const showRoom = (
+    cli: MatrixClient,
+    hierarchy: RoomHierarchy,
+    roomId: string,
+    autoJoin = false,
+    roomType?: RoomType,
+) => {
     const room = hierarchy.roomMap.get(roomId);
 
     // Don't let the user view a room they won't be able to either peek or join:
@@ -303,7 +318,8 @@ export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
             avatarUrl: room.avatar_url,
             // XXX: This logic is duplicated from the JS SDK which would normally decide what the name is.
             name: room.name || roomAlias || _t("Unnamed room"),
-        },
+            roomType,
+        } as IOOBData,
     });
 };
 
@@ -313,7 +329,7 @@ interface IHierarchyLevelProps {
     hierarchy: RoomHierarchy;
     parents: Set<string>;
     selectedMap?: Map<string, Set<string>>;
-    onViewRoomClick(roomId: string, autoJoin: boolean): void;
+    onViewRoomClick(roomId: string, autoJoin: boolean, roomType?: RoomType): void;
     onToggleClick?(parentId: string, childId: string): void;
 }
 
@@ -351,8 +367,8 @@ export const HierarchyLevel = ({
                     room={room}
                     suggested={hierarchy.isSuggested(root.room_id, room.room_id)}
                     selected={selectedMap?.get(root.room_id)?.has(room.room_id)}
-                    onViewRoomClick={(autoJoin) => {
-                        onViewRoomClick(room.room_id, autoJoin);
+                    onViewRoomClick={(autoJoin, roomType) => {
+                        onViewRoomClick(room.room_id, autoJoin, roomType);
                     }}
                     hasPermissions={hasPermissions}
                     onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, room.room_id) : undefined}
@@ -371,8 +387,8 @@ export const HierarchyLevel = ({
                     }).length}
                     suggested={hierarchy.isSuggested(root.room_id, space.room_id)}
                     selected={selectedMap?.get(root.room_id)?.has(space.room_id)}
-                    onViewRoomClick={(autoJoin) => {
-                        onViewRoomClick(space.room_id, autoJoin);
+                    onViewRoomClick={(autoJoin, roomType) => {
+                        onViewRoomClick(space.room_id, autoJoin, roomType);
                     }}
                     hasPermissions={hasPermissions}
                     onToggleClick={onToggleClick ? () => onToggleClick(root.room_id, space.room_id) : undefined}
@@ -574,7 +590,7 @@ const SpaceHierarchy = ({
     const { loading, rooms, hierarchy, loadMore } = useSpaceSummary(space);
 
     const filteredRoomSet = useMemo<Set<IHierarchyRoom>>(() => {
-        if (!rooms.length) return new Set();
+        if (!rooms?.length) return new Set();
         const lcQuery = query.toLowerCase().trim();
         if (!lcQuery) return new Set(rooms);
 
@@ -650,8 +666,8 @@ const SpaceHierarchy = ({
                             parents={new Set()}
                             selectedMap={selected}
                             onToggleClick={hasPermissions ? onToggleClick : undefined}
-                            onViewRoomClick={(roomId, autoJoin) => {
-                                showRoom(cli, hierarchy, roomId, autoJoin);
+                            onViewRoomClick={(roomId, autoJoin, roomType) => {
+                                showRoom(cli, hierarchy, roomId, autoJoin, roomType);
                             }}
                         />
                     </>;
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 6bb0433448..4dfb1ddad8 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -78,6 +78,7 @@ import { CreateEventField, IGroupSummary } from "../views/dialogs/CreateSpaceFro
 import { useAsyncMemo } from "../../hooks/useAsyncMemo";
 import Spinner from "../views/elements/Spinner";
 import GroupAvatar from "../views/avatars/GroupAvatar";
+import { useDispatcher } from "../../hooks/useDispatcher";
 
 interface IProps {
     space: Room;
@@ -155,10 +156,10 @@ const SpaceInfo = ({ space }) => {
     </div>;
 };
 
-const onBetaClick = () => {
+const onPreferencesClick = () => {
     defaultDispatcher.dispatch({
         action: Action.ViewUserSettings,
-        initialTabId: UserTab.Labs,
+        initialTabId: UserTab.Preferences,
     });
 };
 
@@ -191,6 +192,11 @@ interface ISpacePreviewProps {
 const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => {
     const cli = useContext(MatrixClientContext);
     const myMembership = useMyRoomMembership(space);
+    useDispatcher(defaultDispatcher, payload => {
+        if (payload.action === Action.JoinRoomError && payload.roomId === space.roomId) {
+            setBusy(false); // stop the spinner, join failed
+        }
+    });
 
     const [busy, setBusy] = useState(false);
 
@@ -280,15 +286,11 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
     if (!spacesEnabled) {
         footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
             { myMembership === "join"
-                ? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
-                    spaceName: space.name,
-                }, {
-                    a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
+                ? _t("To view this Space, hide communities in your <a>preferences</a>", {}, {
+                    a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
                 })
-                : _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
-                    spaceName: space.name,
-                }, {
-                    a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
+                : _t("To join this Space, hide communities in your <a>preferences</a>", {}, {
+                    a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
                 })
             }
         </div>;
@@ -725,7 +727,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
         </div>
 
         <div className="mx_SpaceRoomView_inviteTeammates_betaDisclaimer">
-            <BetaPill onClick={onBetaClick} />
+            <BetaPill />
             { _t("<b>This is an experimental feature.</b> For now, " +
                 "new users receiving an invite will have to open the invite on <link/> to actually join.", {}, {
                 b: sub => <b>{ sub }</b>,
diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx
index a0bccfdce9..ccf9d9d416 100644
--- a/src/components/structures/ThreadPanel.tsx
+++ b/src/components/structures/ThreadPanel.tsx
@@ -16,7 +16,7 @@ limitations under the License.
 
 import React from 'react';
 import { MatrixEvent, Room } from 'matrix-js-sdk/src';
-import { Thread } from 'matrix-js-sdk/src/models/thread';
+import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
 
 import BaseCard from "../views/right_panel/BaseCard";
 import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
@@ -46,13 +46,13 @@ export default class ThreadPanel extends React.Component<IProps, IState> {
     }
 
     public componentDidMount(): void {
-        this.room.on("Thread.update", this.onThreadEventReceived);
-        this.room.on("Thread.ready", this.onThreadEventReceived);
+        this.room.on(ThreadEvent.Update, this.onThreadEventReceived);
+        this.room.on(ThreadEvent.Ready, this.onThreadEventReceived);
     }
 
     public componentWillUnmount(): void {
-        this.room.removeListener("Thread.update", this.onThreadEventReceived);
-        this.room.removeListener("Thread.ready", this.onThreadEventReceived);
+        this.room.removeListener(ThreadEvent.Update, this.onThreadEventReceived);
+        this.room.removeListener(ThreadEvent.Ready, this.onThreadEventReceived);
     }
 
     private onThreadEventReceived = () => this.updateThreads();
diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx
index 614d3c9f4b..dda4c06417 100644
--- a/src/components/structures/ThreadView.tsx
+++ b/src/components/structures/ThreadView.tsx
@@ -16,7 +16,7 @@ limitations under the License.
 
 import React from 'react';
 import { MatrixEvent, Room } from 'matrix-js-sdk/src';
-import { Thread } from 'matrix-js-sdk/src/models/thread';
+import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
 
 import BaseCard from "../views/right_panel/BaseCard";
 import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
@@ -99,15 +99,15 @@ export default class ThreadView extends React.Component<IProps, IState> {
             thread = new Thread([mxEv], this.props.room, client);
             mxEv.setThread(thread);
         }
-        thread.on("Thread.update", this.updateThread);
-        thread.once("Thread.ready", this.updateThread);
+        thread.on(ThreadEvent.Update, this.updateThread);
+        thread.once(ThreadEvent.Ready, this.updateThread);
         this.updateThread(thread);
     };
 
     private teardownThread = () => {
         if (this.state.thread) {
-            this.state.thread.removeListener("Thread.update", this.updateThread);
-            this.state.thread.removeListener("Thread.ready", this.updateThread);
+            this.state.thread.removeListener(ThreadEvent.Update, this.updateThread);
+            this.state.thread.removeListener(ThreadEvent.Ready, this.updateThread);
         }
     };
 
diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.tsx
similarity index 72%
rename from src/components/structures/UserView.js
rename to src/components/structures/UserView.tsx
index eb839be7be..0b686995fd 100644
--- a/src/components/structures/UserView.js
+++ b/src/components/structures/UserView.tsx
@@ -16,52 +16,60 @@ limitations under the License.
 */
 
 import React from "react";
-import PropTypes from "prop-types";
 import { MatrixClientPeg } from "../../MatrixClientPeg";
-import * as sdk from "../../index";
 import Modal from '../../Modal';
 import { _t } from '../../languageHandler';
 import HomePage from "./HomePage";
 import { replaceableComponent } from "../../utils/replaceableComponent";
 import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import ErrorDialog from "../views/dialogs/ErrorDialog";
+import MainSplit from "./MainSplit";
+import RightPanel from "./RightPanel";
+import Spinner from "../views/elements/Spinner";
+import ResizeNotifier from "../../utils/ResizeNotifier";
+
+interface IProps {
+    userId?: string;
+    resizeNotifier: ResizeNotifier;
+}
+
+interface IState {
+    loading: boolean;
+    member?: RoomMember;
+}
 
 @replaceableComponent("structures.UserView")
-export default class UserView extends React.Component {
-    static get propTypes() {
-        return {
-            userId: PropTypes.string,
+export default class UserView extends React.Component<IProps, IState> {
+    constructor(props: IProps) {
+        super(props);
+        this.state = {
+            loading: true,
         };
     }
 
-    constructor(props) {
-        super(props);
-        this.state = {};
-    }
-
-    componentDidMount() {
+    public componentDidMount(): void {
         if (this.props.userId) {
-            this._loadProfileInfo();
+            this.loadProfileInfo();
         }
     }
 
-    componentDidUpdate(prevProps) {
+    public componentDidUpdate(prevProps: IProps): void {
         // XXX: We shouldn't need to null check the userId here, but we declare
         // it as optional and MatrixChat sometimes fires in a way which results
         // in an NPE when we try to update the profile info.
         if (prevProps.userId !== this.props.userId && this.props.userId) {
-            this._loadProfileInfo();
+            this.loadProfileInfo();
         }
     }
 
-    async _loadProfileInfo() {
+    private async loadProfileInfo(): Promise<void> {
         const cli = MatrixClientPeg.get();
         this.setState({ loading: true });
         let profileInfo;
         try {
             profileInfo = await cli.getProfileInfo(this.props.userId);
         } catch (err) {
-            const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
             Modal.createTrackedDialog(_t('Could not load user profile'), '', ErrorDialog, {
                 title: _t('Could not load user profile'),
                 description: ((err && err.message) ? err.message : _t("Operation failed")),
@@ -75,14 +83,11 @@ export default class UserView extends React.Component {
         this.setState({ member, loading: false });
     }
 
-    render() {
+    public render(): JSX.Element {
         if (this.state.loading) {
-            const Spinner = sdk.getComponent("elements.Spinner");
             return <Spinner />;
-        } else if (this.state.member) {
-            const RightPanel = sdk.getComponent('structures.RightPanel');
-            const MainSplit = sdk.getComponent('structures.MainSplit');
-            const panel = <RightPanel user={this.state.member} />;
+        } else if (this.state.member?.user) {
+            const panel = <RightPanel user={this.state.member.user} resizeNotifier={this.props.resizeNotifier} />;
             return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
                 <HomePage />
             </MainSplit>);
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.tsx
similarity index 90%
rename from src/components/structures/ViewSource.js
rename to src/components/structures/ViewSource.tsx
index 2bfa20e892..20bbece755 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.tsx
@@ -17,24 +17,28 @@ limitations under the License.
 */
 
 import React from "react";
-import PropTypes from "prop-types";
 import SyntaxHighlight from "../views/elements/SyntaxHighlight";
 import { _t } from "../../languageHandler";
-import * as sdk from "../../index";
 import MatrixClientContext from "../../contexts/MatrixClientContext";
 import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
 import { canEditContent } from "../../utils/EventUtils";
 import { MatrixClientPeg } from '../../MatrixClientPeg';
 import { replaceableComponent } from "../../utils/replaceableComponent";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { IDialogProps } from "../views/dialogs/IDialogProps";
+import BaseDialog from "../views/dialogs/BaseDialog";
+
+interface IProps extends IDialogProps {
+    mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
+}
+
+interface IState {
+    isEditing: boolean;
+}
 
 @replaceableComponent("structures.ViewSource")
-export default class ViewSource extends React.Component {
-    static propTypes = {
-        onFinished: PropTypes.func.isRequired,
-        mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
-    };
-
-    constructor(props) {
+export default class ViewSource extends React.Component<IProps, IState> {
+    constructor(props: IProps) {
         super(props);
 
         this.state = {
@@ -42,19 +46,20 @@ export default class ViewSource extends React.Component {
         };
     }
 
-    onBack() {
+    private onBack(): void {
         // TODO: refresh the "Event ID:" modal header
         this.setState({ isEditing: false });
     }
 
-    onEdit() {
+    private onEdit(): void {
         this.setState({ isEditing: true });
     }
 
     // returns the dialog body for viewing the event source
-    viewSourceContent() {
+    private viewSourceContent(): JSX.Element {
         const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
         const isEncrypted = mxEvent.isEncrypted();
+        // @ts-ignore
         const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
         const originalEventSource = mxEvent.event;
 
@@ -86,7 +91,7 @@ export default class ViewSource extends React.Component {
     }
 
     // returns the id of the initial message, not the id of the previous edit
-    getBaseEventId() {
+    private getBaseEventId(): string {
         const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
         const isEncrypted = mxEvent.isEncrypted();
         const baseMxEvent = this.props.mxEvent;
@@ -100,7 +105,7 @@ export default class ViewSource extends React.Component {
     }
 
     // returns the SendCustomEvent component prefilled with the correct details
-    editSourceContent() {
+    private editSourceContent(): JSX.Element {
         const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
 
         const isStateEvent = mxEvent.isState();
@@ -159,14 +164,13 @@ export default class ViewSource extends React.Component {
         }
     }
 
-    canSendStateEvent(mxEvent) {
+    private canSendStateEvent(mxEvent: MatrixEvent): boolean {
         const cli = MatrixClientPeg.get();
         const room = cli.getRoom(mxEvent.getRoomId());
         return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
     }
 
-    render() {
-        const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
+    public render(): JSX.Element {
         const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
 
         const isEditing = this.state.isEditing;
diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.tsx
similarity index 76%
rename from src/components/views/avatars/MemberStatusMessageAvatar.js
rename to src/components/views/avatars/MemberStatusMessageAvatar.tsx
index 82b7b8e400..8c703b3b32 100644
--- a/src/components/views/avatars/MemberStatusMessageAvatar.js
+++ b/src/components/views/avatars/MemberStatusMessageAvatar.tsx
@@ -15,43 +15,48 @@ limitations under the License.
 */
 
 import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
 import { _t } from "../../../languageHandler";
 import MemberAvatar from '../avatars/MemberAvatar';
 import classNames from 'classnames';
 import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
 import SettingsStore from "../../../settings/SettingsStore";
-import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
+import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
+
+interface IProps {
+    member: RoomMember;
+    width?: number;
+    height?: number;
+    resizeMethod?: ResizeMethod;
+}
+
+interface IState {
+    hasStatus: boolean;
+    menuDisplayed: boolean;
+}
 
 @replaceableComponent("views.avatars.MemberStatusMessageAvatar")
-export default class MemberStatusMessageAvatar extends React.Component {
-    static propTypes = {
-        member: PropTypes.object.isRequired,
-        width: PropTypes.number,
-        height: PropTypes.number,
-        resizeMethod: PropTypes.string,
-    };
-
-    static defaultProps = {
+export default class MemberStatusMessageAvatar extends React.Component<IProps, IState> {
+    public static defaultProps: Partial<IProps> = {
         width: 40,
         height: 40,
         resizeMethod: 'crop',
     };
+    private button = createRef<HTMLDivElement>();
 
-    constructor(props) {
+    constructor(props: IProps) {
         super(props);
 
         this.state = {
             hasStatus: this.hasStatus,
             menuDisplayed: false,
         };
-
-        this._button = createRef();
     }
 
-    componentDidMount() {
+    public componentDidMount(): void {
         if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
             throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
         }
@@ -62,44 +67,44 @@ export default class MemberStatusMessageAvatar extends React.Component {
         if (!user) {
             return;
         }
-        user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
+        user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
     }
 
-    componentWillUnmount() {
+    public componentWillUnmount(): void {
         const { user } = this.props.member;
         if (!user) {
             return;
         }
         user.removeListener(
             "User._unstable_statusMessage",
-            this._onStatusMessageCommitted,
+            this.onStatusMessageCommitted,
         );
     }
 
-    get hasStatus() {
+    private get hasStatus(): boolean {
         const { user } = this.props.member;
         if (!user) {
             return false;
         }
-        return !!user._unstable_statusMessage;
+        return !!user.unstable_statusMessage;
     }
 
-    _onStatusMessageCommitted = () => {
+    private onStatusMessageCommitted = (): void => {
         // The `User` object has observed a status message change.
         this.setState({
             hasStatus: this.hasStatus,
         });
     };
 
-    openMenu = () => {
+    private openMenu = (): void => {
         this.setState({ menuDisplayed: true });
     };
 
-    closeMenu = () => {
+    private closeMenu = (): void => {
         this.setState({ menuDisplayed: false });
     };
 
-    render() {
+    public render(): JSX.Element {
         const avatar = <MemberAvatar
             member={this.props.member}
             width={this.props.width}
@@ -118,7 +123,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
 
         let contextMenu;
         if (this.state.menuDisplayed) {
-            const elementRect = this._button.current.getBoundingClientRect();
+            const elementRect = this.button.current.getBoundingClientRect();
 
             const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
             const chevronMargin = 1; // Add some spacing away from target
@@ -126,13 +131,13 @@ export default class MemberStatusMessageAvatar extends React.Component {
             contextMenu = (
                 <ContextMenu
                     chevronOffset={(elementRect.width - chevronWidth) / 2}
-                    chevronFace="bottom"
+                    chevronFace={ChevronFace.Bottom}
                     left={elementRect.left + window.pageXOffset}
                     top={elementRect.top + window.pageYOffset - chevronMargin}
                     menuWidth={226}
                     onFinished={this.closeMenu}
                 >
-                    <StatusMessageContextMenu user={this.props.member.user} onFinished={this.closeMenu} />
+                    <StatusMessageContextMenu user={this.props.member.user} />
                 </ContextMenu>
             );
         }
@@ -140,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
         return <React.Fragment>
             <ContextMenuButton
                 className={classes}
-                inputRef={this._button}
+                inputRef={this.button}
                 onClick={this.openMenu}
                 isExpanded={this.state.menuDisplayed}
                 label={_t("User Status")}
diff --git a/src/components/views/context_menus/GenericElementContextMenu.js b/src/components/views/context_menus/GenericElementContextMenu.tsx
similarity index 67%
rename from src/components/views/context_menus/GenericElementContextMenu.js
rename to src/components/views/context_menus/GenericElementContextMenu.tsx
index 87d44ef0d3..a0a8c89b37 100644
--- a/src/components/views/context_menus/GenericElementContextMenu.js
+++ b/src/components/views/context_menus/GenericElementContextMenu.tsx
@@ -15,45 +15,41 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
-/*
+interface IProps {
+    element: React.ReactNode;
+    // Function to be called when the parent window is resized
+    // This can be used to reposition or close the menu on resize and
+    // ensure that it is not displayed in a stale position.
+    onResize?: () => void;
+}
+
+/**
  * This component can be used to display generic HTML content in a contextual
  * menu.
  */
-
 @replaceableComponent("views.context_menus.GenericElementContextMenu")
-export default class GenericElementContextMenu extends React.Component {
-    static propTypes = {
-        element: PropTypes.element.isRequired,
-        // Function to be called when the parent window is resized
-        // This can be used to reposition or close the menu on resize and
-        // ensure that it is not displayed in a stale position.
-        onResize: PropTypes.func,
-    };
-
-    constructor(props) {
+export default class GenericElementContextMenu extends React.Component<IProps> {
+    constructor(props: IProps) {
         super(props);
-        this.resize = this.resize.bind(this);
     }
 
-    componentDidMount() {
-        this.resize = this.resize.bind(this);
+    public componentDidMount(): void {
         window.addEventListener("resize", this.resize);
     }
 
-    componentWillUnmount() {
+    public componentWillUnmount(): void {
         window.removeEventListener("resize", this.resize);
     }
 
-    resize() {
+    private resize = (): void => {
         if (this.props.onResize) {
             this.props.onResize();
         }
-    }
+    };
 
-    render() {
+    public render(): JSX.Element {
         return <div>{ this.props.element }</div>;
     }
 }
diff --git a/src/components/views/context_menus/GenericTextContextMenu.js b/src/components/views/context_menus/GenericTextContextMenu.tsx
similarity index 86%
rename from src/components/views/context_menus/GenericTextContextMenu.js
rename to src/components/views/context_menus/GenericTextContextMenu.tsx
index 474732e88b..3ca158dd02 100644
--- a/src/components/views/context_menus/GenericTextContextMenu.js
+++ b/src/components/views/context_menus/GenericTextContextMenu.tsx
@@ -15,16 +15,15 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
-@replaceableComponent("views.context_menus.GenericTextContextMenu")
-export default class GenericTextContextMenu extends React.Component {
-    static propTypes = {
-        message: PropTypes.string.isRequired,
-    };
+interface IProps {
+    message: string;
+}
 
-    render() {
+@replaceableComponent("views.context_menus.GenericTextContextMenu")
+export default class GenericTextContextMenu extends React.Component<IProps> {
+    public render(): JSX.Element {
         return <div>{ this.props.message }</div>;
     }
 }
diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.tsx
similarity index 71%
rename from src/components/views/context_menus/StatusMessageContextMenu.js
rename to src/components/views/context_menus/StatusMessageContextMenu.tsx
index e05b05116c..954dc3f5c0 100644
--- a/src/components/views/context_menus/StatusMessageContextMenu.js
+++ b/src/components/views/context_menus/StatusMessageContextMenu.tsx
@@ -14,53 +14,59 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
-import PropTypes from 'prop-types';
+import React, { ChangeEvent } from 'react';
 import { _t } from '../../../languageHandler';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
-import * as sdk from '../../../index';
-import AccessibleButton from '../elements/AccessibleButton';
+import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { User } from "matrix-js-sdk/src/models/user";
+import Spinner from "../elements/Spinner";
+
+interface IProps {
+    // js-sdk User object. Not required because it might not exist.
+    user?: User;
+}
+
+interface IState {
+    message: string;
+    waiting: boolean;
+}
 
 @replaceableComponent("views.context_menus.StatusMessageContextMenu")
-export default class StatusMessageContextMenu extends React.Component {
-    static propTypes = {
-        // js-sdk User object. Not required because it might not exist.
-        user: PropTypes.object,
-    };
-
-    constructor(props) {
+export default class StatusMessageContextMenu extends React.Component<IProps, IState> {
+    constructor(props: IProps) {
         super(props);
 
         this.state = {
             message: this.comittedStatusMessage,
+            waiting: false,
         };
     }
 
-    componentDidMount() {
+    public componentDidMount(): void {
         const { user } = this.props;
         if (!user) {
             return;
         }
-        user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
+        user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
     }
 
-    componentWillUnmount() {
+    public componentWillUnmount(): void {
         const { user } = this.props;
         if (!user) {
             return;
         }
         user.removeListener(
             "User._unstable_statusMessage",
-            this._onStatusMessageCommitted,
+            this.onStatusMessageCommitted,
         );
     }
 
-    get comittedStatusMessage() {
-        return this.props.user ? this.props.user._unstable_statusMessage : "";
+    get comittedStatusMessage(): string {
+        return this.props.user ? this.props.user.unstable_statusMessage : "";
     }
 
-    _onStatusMessageCommitted = () => {
+    private onStatusMessageCommitted = (): void => {
         // The `User` object has observed a status message change.
         this.setState({
             message: this.comittedStatusMessage,
@@ -68,14 +74,14 @@ export default class StatusMessageContextMenu extends React.Component {
         });
     };
 
-    _onClearClick = (e) => {
+    private onClearClick = (): void=> {
         MatrixClientPeg.get()._unstable_setStatusMessage("");
         this.setState({
             waiting: true,
         });
     };
 
-    _onSubmit = (e) => {
+    private onSubmit = (e: ButtonEvent): void => {
         e.preventDefault();
         MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
         this.setState({
@@ -83,27 +89,25 @@ export default class StatusMessageContextMenu extends React.Component {
         });
     };
 
-    _onStatusChange = (e) => {
+    private onStatusChange = (e: ChangeEvent): void => {
         // The input field's value was changed.
         this.setState({
-            message: e.target.value,
+            message: (e.target as HTMLInputElement).value,
         });
     };
 
-    render() {
-        const Spinner = sdk.getComponent('views.elements.Spinner');
-
+    public render(): JSX.Element {
         let actionButton;
         if (this.comittedStatusMessage) {
             if (this.state.message === this.comittedStatusMessage) {
                 actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
-                    onClick={this._onClearClick}
+                    onClick={this.onClearClick}
                 >
                     <span>{ _t("Clear status") }</span>
                 </AccessibleButton>;
             } else {
                 actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
-                    onClick={this._onSubmit}
+                    onClick={this.onSubmit}
                 >
                     <span>{ _t("Update status") }</span>
                 </AccessibleButton>;
@@ -112,7 +116,7 @@ export default class StatusMessageContextMenu extends React.Component {
             actionButton = <AccessibleButton
                 className="mx_StatusMessageContextMenu_submit"
                 disabled={!this.state.message}
-                onClick={this._onSubmit}
+                onClick={this.onSubmit}
             >
                 <span>{ _t("Set status") }</span>
             </AccessibleButton>;
@@ -120,13 +124,13 @@ export default class StatusMessageContextMenu extends React.Component {
 
         let spinner = null;
         if (this.state.waiting) {
-            spinner = <Spinner w="24" h="24" />;
+            spinner = <Spinner w={24} h={24} />;
         }
 
         const form = <form
             className="mx_StatusMessageContextMenu_form"
             autoComplete="off"
-            onSubmit={this._onSubmit}
+            onSubmit={this.onSubmit}
         >
             <input
                 type="text"
@@ -134,9 +138,9 @@ export default class StatusMessageContextMenu extends React.Component {
                 key="message"
                 placeholder={_t("Set a new status...")}
                 autoFocus={true}
-                maxLength="60"
+                maxLength={60}
                 value={this.state.message}
-                onChange={this._onStatusChange}
+                onChange={this.onStatusChange}
             />
             <div className="mx_StatusMessageContextMenu_actionContainer">
                 { actionButton }
diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
index cf4f369d09..01a767bf14 100644
--- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
+++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
@@ -258,7 +258,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
             className="mx_textinput_icon mx_textinput_search"
             placeholder={filterPlaceholder}
             onSearch={setQuery}
-            autoComplete={true}
             autoFocus={true}
         />
         <AutoHideScrollbar className="mx_AddExistingToSpace_content">
diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx
index d03b668cd9..3bb78233ea 100644
--- a/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx
+++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx
@@ -23,10 +23,9 @@ import Modal from '../../../Modal';
 import BaseDialog from "./BaseDialog";
 import DialogButtons from "../elements/DialogButtons";
 import QuestionDialog from "./QuestionDialog";
+import { IDialogProps } from "./IDialogProps";
 
-interface IProps {
-    onFinished: (success: boolean) => void;
-}
+interface IProps extends IDialogProps {}
 
 const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
     const brand = SdkConfig.get().brand;
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 77e2b6ae0c..7f08a3eb58 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -243,7 +243,6 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
                 className="mx_textinput_icon mx_textinput_search"
                 placeholder={_t("Search for rooms or people")}
                 onSearch={setQuery}
-                autoComplete={true}
                 autoFocus={true}
             />
             <AutoHideScrollbar className="mx_ForwardList_content">
diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx
index 3a8cd53945..94ae71f1c9 100644
--- a/src/components/views/dialogs/LeaveSpaceDialog.tsx
+++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx
@@ -57,7 +57,6 @@ const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
             className="mx_textinput_icon mx_textinput_search"
             placeholder={filterPlaceholder}
             onSearch={setQuery}
-            autoComplete={true}
             autoFocus={true}
         />
         <AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
@@ -98,13 +97,13 @@ const LeaveRoomsPicker = ({ space, spaceChildren, roomsToLeave, setRoomsToLeave
             definitions={[
                 {
                     value: RoomsToLeave.None,
-                    label: _t("Don't leave any"),
+                    label: _t("Don't leave any rooms"),
                 }, {
                     value: RoomsToLeave.All,
-                    label: _t("Leave all rooms and spaces"),
+                    label: _t("Leave all rooms"),
                 }, {
                     value: RoomsToLeave.Specific,
-                    label: _t("Leave specific rooms and spaces"),
+                    label: _t("Leave some rooms"),
                 },
             ]}
         />
@@ -167,11 +166,13 @@ const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
     >
         <div className="mx_Dialog_content" id="mx_LeaveSpaceDialog">
             <p>
-                { _t("Are you sure you want to leave <spaceName/>?", {}, {
+                { _t("You are about to leave <spaceName/>.", {}, {
                     spaceName: () => <b>{ space.name }</b>,
                 }) }
                 &nbsp;
                 { rejoinWarning }
+                { rejoinWarning && (<>&nbsp;</>) }
+                { spaceChildren.length > 0 && _t("Would you like to leave the rooms in this space?") }
             </p>
 
             { spaceChildren.length > 0 && <LeaveRoomsPicker
diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
index c63fdc4c84..dd5c549bbe 100644
--- a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
+++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx
@@ -126,7 +126,6 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
                 className="mx_textinput_icon mx_textinput_search"
                 placeholder={_t("Search spaces")}
                 onSearch={setQuery}
-                autoComplete={true}
                 autoFocus={true}
             />
             <AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx
index 8389757347..8c503e340d 100644
--- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx
+++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx
@@ -19,7 +19,7 @@ import { User } from "matrix-js-sdk/src/models/user";
 
 import { _t } from "../../../languageHandler";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import E2EIcon from "../rooms/E2EIcon";
+import E2EIcon, { E2EState } from "../rooms/E2EIcon";
 import AccessibleButton from "../elements/AccessibleButton";
 import BaseDialog from "./BaseDialog";
 import { IDialogProps } from "./IDialogProps";
@@ -47,7 +47,7 @@ const UntrustedDeviceDialog: React.FC<IProps> = ({ device, user, onFinished }) =
         onFinished={onFinished}
         className="mx_UntrustedDeviceDialog"
         title={<>
-            <E2EIcon status="warning" size={24} hideTooltip={true} />
+            <E2EIcon status={E2EState.Warning} size={24} hideTooltip={true} />
             { _t("Not Trusted") }
         </>}
     >
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.tsx
similarity index 74%
rename from src/components/views/elements/AppTile.js
rename to src/components/views/elements/AppTile.tsx
index a02465d01e..9ab7a91788 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.tsx
@@ -19,7 +19,6 @@ limitations under the License.
 
 import url from 'url';
 import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
 import AccessibleButton from './AccessibleButton';
 import { _t } from '../../../languageHandler';
@@ -39,33 +38,95 @@ import { MatrixCapabilities } from "matrix-widget-api";
 import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
 import WidgetAvatar from "../avatars/WidgetAvatar";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { IApp } from "../../../stores/WidgetStore";
+
+interface IProps {
+    app: IApp;
+    // If room is not specified then it is an account level widget
+    // which bypasses permission prompts as it was added explicitly by that user
+    room: Room;
+    // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
+    // This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
+    fullWidth?: boolean;
+    // Optional. If set, renders a smaller view of the widget
+    miniMode?: boolean;
+    // UserId of the current user
+    userId: string;
+    // UserId of the entity that added / modified the widget
+    creatorUserId: string;
+    waitForIframeLoad: boolean;
+    showMenubar?: boolean;
+    // Optional onEditClickHandler (overrides default behaviour)
+    onEditClick?: () => void;
+    // Optional onDeleteClickHandler (overrides default behaviour)
+    onDeleteClick?: () => void;
+    // Optionally hide the tile title
+    showTitle?: boolean;
+    // Optionally handle minimise button pointer events (default false)
+    handleMinimisePointerEvents?: boolean;
+    // Optionally hide the popout widget icon
+    showPopout?: boolean;
+    // Is this an instance of a user widget
+    userWidget: boolean;
+    // sets the pointer-events property on the iframe
+    pointerEvents?: string;
+    widgetPageTitle?: string;
+}
+
+interface IState {
+    initialising: boolean; // True while we are mangling the widget URL
+    // True while the iframe content is loading
+    loading: boolean;
+    // Assume that widget has permission to load if we are the user who
+    // added it to the room, or if explicitly granted by the user
+    hasPermissionToLoad: boolean;
+    error: Error;
+    menuDisplayed: boolean;
+    widgetPageTitle: string;
+}
 
 @replaceableComponent("views.elements.AppTile")
-export default class AppTile extends React.Component {
-    constructor(props) {
+export default class AppTile extends React.Component<IProps, IState> {
+    public static defaultProps: Partial<IProps> = {
+        waitForIframeLoad: true,
+        showMenubar: true,
+        showTitle: true,
+        showPopout: true,
+        handleMinimisePointerEvents: false,
+        userWidget: false,
+        miniMode: false,
+    };
+
+    private contextMenuButton = createRef<any>();
+    private iframe: HTMLIFrameElement; // ref to the iframe (callback style)
+    private allowedWidgetsWatchRef: string;
+    private persistKey: string;
+    private sgWidget: StopGapWidget;
+    private dispatcherRef: string;
+
+    constructor(props: IProps) {
         super(props);
 
         // The key used for PersistedElement
-        this._persistKey = getPersistKey(this.props.app.id);
+        this.persistKey = getPersistKey(this.props.app.id);
         try {
-            this._sgWidget = new StopGapWidget(this.props);
-            this._sgWidget.on("preparing", this._onWidgetPrepared);
-            this._sgWidget.on("ready", this._onWidgetReady);
+            this.sgWidget = new StopGapWidget(this.props);
+            this.sgWidget.on("preparing", this.onWidgetPrepared);
+            this.sgWidget.on("ready", this.onWidgetReady);
         } catch (e) {
             console.log("Failed to construct widget", e);
-            this._sgWidget = null;
+            this.sgWidget = null;
         }
-        this.iframe = null; // ref to the iframe (callback style)
 
-        this.state = this._getNewState(props);
-        this._contextMenuButton = createRef();
+        this.state = this.getNewState(props);
 
-        this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
+        this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
     }
 
     // This is a function to make the impact of calling SettingsStore slightly less
-    hasPermissionToLoad = (props) => {
-        if (this._usingLocalWidget()) return true;
+    private hasPermissionToLoad = (props: IProps): boolean => {
+        if (this.usingLocalWidget()) return true;
         if (!props.room) return true; // user widgets always have permissions
 
         const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
@@ -81,34 +142,34 @@ export default class AppTile extends React.Component {
      * @param  {Object} newProps The new properties of the component
      * @return {Object} Updated component state to be set with setState
      */
-    _getNewState(newProps) {
+    private getNewState(newProps: IProps): IState {
         return {
             initialising: true, // True while we are mangling the widget URL
             // True while the iframe content is loading
-            loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
+            loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this.persistKey),
             // Assume that widget has permission to load if we are the user who
             // added it to the room, or if explicitly granted by the user
             hasPermissionToLoad: this.hasPermissionToLoad(newProps),
             error: null,
-            widgetPageTitle: newProps.widgetPageTitle,
             menuDisplayed: false,
+            widgetPageTitle: this.props.widgetPageTitle,
         };
     }
 
-    onAllowedWidgetsChange = () => {
+    private onAllowedWidgetsChange = (): void => {
         const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
 
         if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
             // Force the widget to be non-persistent (able to be deleted/forgotten)
             ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
-            PersistedElement.destroyElement(this._persistKey);
-            if (this._sgWidget) this._sgWidget.stop();
+            PersistedElement.destroyElement(this.persistKey);
+            if (this.sgWidget) this.sgWidget.stop();
         }
 
         this.setState({ hasPermissionToLoad });
     };
 
-    isMixedContent() {
+    private isMixedContent(): boolean {
         const parentContentProtocol = window.location.protocol;
         const u = url.parse(this.props.app.url);
         const childContentProtocol = u.protocol;
@@ -120,69 +181,70 @@ export default class AppTile extends React.Component {
         return false;
     }
 
-    componentDidMount() {
+    public componentDidMount(): void {
         // Only fetch IM token on mount if we're showing and have permission to load
-        if (this._sgWidget && this.state.hasPermissionToLoad) {
-            this._startWidget();
+        if (this.sgWidget && this.state.hasPermissionToLoad) {
+            this.startWidget();
         }
 
         // Widget action listeners
-        this.dispatcherRef = dis.register(this._onAction);
+        this.dispatcherRef = dis.register(this.onAction);
     }
 
-    componentWillUnmount() {
+    public componentWillUnmount(): void {
         // Widget action listeners
         if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
 
         // if it's not remaining on screen, get rid of the PersistedElement container
         if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
             ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
-            PersistedElement.destroyElement(this._persistKey);
+            PersistedElement.destroyElement(this.persistKey);
         }
 
-        if (this._sgWidget) {
-            this._sgWidget.stop();
+        if (this.sgWidget) {
+            this.sgWidget.stop();
         }
 
-        SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef);
+        SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
     }
 
-    _resetWidget(newProps) {
-        if (this._sgWidget) {
-            this._sgWidget.stop();
+    private resetWidget(newProps: IProps): void {
+        if (this.sgWidget) {
+            this.sgWidget.stop();
         }
         try {
-            this._sgWidget = new StopGapWidget(newProps);
-            this._sgWidget.on("preparing", this._onWidgetPrepared);
-            this._sgWidget.on("ready", this._onWidgetReady);
-            this._startWidget();
+            this.sgWidget = new StopGapWidget(newProps);
+            this.sgWidget.on("preparing", this.onWidgetPrepared);
+            this.sgWidget.on("ready", this.onWidgetReady);
+            this.startWidget();
         } catch (e) {
             console.log("Failed to construct widget", e);
-            this._sgWidget = null;
+            this.sgWidget = null;
         }
     }
 
-    _startWidget() {
-        this._sgWidget.prepare().then(() => {
+    private startWidget(): void {
+        this.sgWidget.prepare().then(() => {
             this.setState({ initialising: false });
         });
     }
 
-    _iframeRefChange = (ref) => {
+    private iframeRefChange = (ref: HTMLIFrameElement): void => {
         this.iframe = ref;
         if (ref) {
-            if (this._sgWidget) this._sgWidget.start(ref);
+            if (this.sgWidget) this.sgWidget.start(ref);
         } else {
-            this._resetWidget(this.props);
+            this.resetWidget(this.props);
         }
     };
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { // eslint-disable-line camelcase
         if (nextProps.app.url !== this.props.app.url) {
-            this._getNewState(nextProps);
+            this.getNewState(nextProps);
             if (this.state.hasPermissionToLoad) {
-                this._resetWidget(nextProps);
+                this.resetWidget(nextProps);
             }
         }
 
@@ -198,7 +260,7 @@ export default class AppTile extends React.Component {
      * @private
      * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
      */
-    async _endWidgetActions() { // widget migration dev note: async to maintain signature
+    private async endWidgetActions(): Promise<void> { // widget migration dev note: async to maintain signature
         // HACK: This is a really dirty way to ensure that Jitsi cleans up
         // its hold on the webcam. Without this, the widget holds a media
         // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
@@ -217,27 +279,27 @@ export default class AppTile extends React.Component {
         }
 
         // Delete the widget from the persisted store for good measure.
-        PersistedElement.destroyElement(this._persistKey);
+        PersistedElement.destroyElement(this.persistKey);
         ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
 
-        if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true });
+        if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
     }
 
-    _onWidgetPrepared = () => {
+    private onWidgetPrepared = (): void => {
         this.setState({ loading: false });
     };
 
-    _onWidgetReady = () => {
+    private onWidgetReady = (): void => {
         if (WidgetType.JITSI.matches(this.props.app.type)) {
-            this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
+            this.sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
         }
     };
 
-    _onAction = payload => {
+    private onAction = (payload): void => {
         if (payload.widgetId === this.props.app.id) {
             switch (payload.action) {
                 case 'm.sticker':
-                    if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
+                    if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
                         dis.dispatch({ action: 'post_sticker_message', data: payload.data });
                         dis.dispatch({ action: 'stickerpicker_close' });
                     } else {
@@ -248,7 +310,7 @@ export default class AppTile extends React.Component {
         }
     };
 
-    _grantWidgetPermission = () => {
+    private grantWidgetPermission = (): void => {
         const roomId = this.props.room.roomId;
         console.info("Granting permission for widget to load: " + this.props.app.eventId);
         const current = SettingsStore.getValue("allowedWidgets", roomId);
@@ -258,14 +320,14 @@ export default class AppTile extends React.Component {
             this.setState({ hasPermissionToLoad: true });
 
             // Fetch a token for the integration manager, now that we're allowed to
-            this._startWidget();
+            this.startWidget();
         }).catch(err => {
             console.error(err);
             // We don't really need to do anything about this - the user will just hit the button again.
         });
     };
 
-    formatAppTileName() {
+    private formatAppTileName(): string {
         let appTileName = "No name";
         if (this.props.app.name && this.props.app.name.trim()) {
             appTileName = this.props.app.name.trim();
@@ -278,11 +340,11 @@ export default class AppTile extends React.Component {
      * actual widget URL
      * @returns {bool} true If using a local version of the widget
      */
-    _usingLocalWidget() {
+    private usingLocalWidget(): boolean {
         return WidgetType.JITSI.matches(this.props.app.type);
     }
 
-    _getTileTitle() {
+    private getTileTitle(): JSX.Element {
         const name = this.formatAppTileName();
         const titleSpacer = <span>&nbsp;-&nbsp;</span>;
         let title = '';
@@ -300,32 +362,32 @@ export default class AppTile extends React.Component {
     }
 
     // TODO replace with full screen interactions
-    _onPopoutWidgetClick = () => {
+    private onPopoutWidgetClick = (): void => {
         // Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
         // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
         if (WidgetType.JITSI.matches(this.props.app.type)) {
-            this._endWidgetActions().then(() => {
+            this.endWidgetActions().then(() => {
                 if (this.iframe) {
                     // Reload iframe
-                    this.iframe.src = this._sgWidget.embedUrl;
+                    this.iframe.src = this.sgWidget.embedUrl;
                 }
             });
         }
         // Using Object.assign workaround as the following opens in a new window instead of a new tab.
         // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
         Object.assign(document.createElement('a'),
-            { target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
+            { target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
     };
 
-    _onContextMenuClick = () => {
+    private onContextMenuClick = (): void => {
         this.setState({ menuDisplayed: true });
     };
 
-    _closeContextMenu = () => {
+    private closeContextMenu = (): void => {
         this.setState({ menuDisplayed: false });
     };
 
-    render() {
+    public render(): JSX.Element {
         let appTileBody;
 
         // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
@@ -351,7 +413,7 @@ export default class AppTile extends React.Component {
                 <Spinner message={_t("Loading...")} />
             </div>
         );
-        if (this._sgWidget === null) {
+        if (this.sgWidget === null) {
             appTileBody = (
                 <div className={appTileBodyClass} style={appTileBodyStyles}>
                     <AppWarning errorMsg={_t("Error loading Widget")} />
@@ -365,9 +427,9 @@ export default class AppTile extends React.Component {
                     <AppPermission
                         roomId={this.props.room.roomId}
                         creatorUserId={this.props.creatorUserId}
-                        url={this._sgWidget.embedUrl}
+                        url={this.sgWidget.embedUrl}
                         isRoomEncrypted={isEncrypted}
-                        onPermissionGranted={this._grantWidgetPermission}
+                        onPermissionGranted={this.grantWidgetPermission}
                     />
                 </div>
             );
@@ -390,8 +452,8 @@ export default class AppTile extends React.Component {
                         { this.state.loading && loadingElement }
                         <iframe
                             allow={iframeFeatures}
-                            ref={this._iframeRefChange}
-                            src={this._sgWidget.embedUrl}
+                            ref={this.iframeRefChange}
+                            src={this.sgWidget.embedUrl}
                             allowFullScreen={true}
                             sandbox={sandboxFlags}
                         />
@@ -407,7 +469,7 @@ export default class AppTile extends React.Component {
                     // Also wrap the PersistedElement in a div to fix the height, otherwise
                     // AppTile's border is in the wrong place
                     appTileBody = <div className="mx_AppTile_persistedWrapper">
-                        <PersistedElement persistKey={this._persistKey}>
+                        <PersistedElement persistKey={this.persistKey}>
                             { appTileBody }
                         </PersistedElement>
                     </div>;
@@ -429,9 +491,9 @@ export default class AppTile extends React.Component {
         if (this.state.menuDisplayed) {
             contextMenu = (
                 <RoomWidgetContextMenu
-                    {...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)}
+                    {...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect(), null)}
                     app={this.props.app}
-                    onFinished={this._closeContextMenu}
+                    onFinished={this.closeContextMenu}
                     showUnpin={!this.props.userWidget}
                     userWidget={this.props.userWidget}
                     onEditClick={this.props.onEditClick}
@@ -444,21 +506,21 @@ export default class AppTile extends React.Component {
             <div className={appTileClasses} id={this.props.app.id}>
                 { this.props.showMenubar &&
                     <div className="mx_AppTileMenuBar">
-                        <span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}>
-                            { this.props.showTitle && this._getTileTitle() }
+                        <span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : "none") }}>
+                            { this.props.showTitle && this.getTileTitle() }
                         </span>
                         <span className="mx_AppTileMenuBarWidgets">
                             { this.props.showPopout && <AccessibleButton
                                 className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
                                 title={_t('Popout widget')}
-                                onClick={this._onPopoutWidgetClick}
+                                onClick={this.onPopoutWidgetClick}
                             /> }
                             <ContextMenuButton
                                 className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
                                 label={_t("Options")}
                                 isExpanded={this.state.menuDisplayed}
-                                inputRef={this._contextMenuButton}
-                                onClick={this._onContextMenuClick}
+                                inputRef={this.contextMenuButton}
+                                onClick={this.onContextMenuClick}
                             />
                         </span>
                     </div> }
@@ -469,49 +531,3 @@ export default class AppTile extends React.Component {
         </React.Fragment>;
     }
 }
-
-AppTile.displayName = 'AppTile';
-
-AppTile.propTypes = {
-    app: PropTypes.object.isRequired,
-    // If room is not specified then it is an account level widget
-    // which bypasses permission prompts as it was added explicitly by that user
-    room: PropTypes.object,
-    // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
-    // This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
-    fullWidth: PropTypes.bool,
-    // Optional. If set, renders a smaller view of the widget
-    miniMode: PropTypes.bool,
-    // UserId of the current user
-    userId: PropTypes.string.isRequired,
-    // UserId of the entity that added / modified the widget
-    creatorUserId: PropTypes.string,
-    waitForIframeLoad: PropTypes.bool,
-    showMenubar: PropTypes.bool,
-    // Optional onEditClickHandler (overrides default behaviour)
-    onEditClick: PropTypes.func,
-    // Optional onDeleteClickHandler (overrides default behaviour)
-    onDeleteClick: PropTypes.func,
-    // Optional onMinimiseClickHandler
-    onMinimiseClick: PropTypes.func,
-    // Optionally hide the tile title
-    showTitle: PropTypes.bool,
-    // Optionally handle minimise button pointer events (default false)
-    handleMinimisePointerEvents: PropTypes.bool,
-    // Optionally hide the popout widget icon
-    showPopout: PropTypes.bool,
-    // Is this an instance of a user widget
-    userWidget: PropTypes.bool,
-    // sets the pointer-events property on the iframe
-    pointerEvents: PropTypes.string,
-};
-
-AppTile.defaultProps = {
-    waitForIframeLoad: true,
-    showMenubar: true,
-    showTitle: true,
-    showPopout: true,
-    handleMinimisePointerEvents: false,
-    userWidget: false,
-    miniMode: false,
-};
diff --git a/src/components/views/elements/AppWarning.js b/src/components/views/elements/AppWarning.tsx
similarity index 60%
rename from src/components/views/elements/AppWarning.js
rename to src/components/views/elements/AppWarning.tsx
index 517242dab5..bac486d4b8 100644
--- a/src/components/views/elements/AppWarning.js
+++ b/src/components/views/elements/AppWarning.tsx
@@ -1,24 +1,20 @@
-import React from 'react'; // eslint-disable-line no-unused-vars
-import PropTypes from 'prop-types';
+import React from 'react';
 
-const AppWarning = (props) => {
+interface IProps {
+    errorMsg?: string;
+}
+
+const AppWarning: React.FC<IProps> = (props) => {
     return (
         <div className='mx_AppPermissionWarning'>
             <div className='mx_AppPermissionWarningImage'>
                 <img src={require("../../../../res/img/warning.svg")} alt='' />
             </div>
             <div className='mx_AppPermissionWarningText'>
-                <span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
+                <span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg || "Error" }</span>
             </div>
         </div>
     );
 };
 
-AppWarning.propTypes = {
-    errorMsg: PropTypes.string,
-};
-AppWarning.defaultProps = {
-    errorMsg: 'Error',
-};
-
 export default AppWarning;
diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.tsx
similarity index 64%
rename from src/components/views/elements/DialogButtons.js
rename to src/components/views/elements/DialogButtons.tsx
index 9dd4a84b9a..0dff64c0b4 100644
--- a/src/components/views/elements/DialogButtons.js
+++ b/src/components/views/elements/DialogButtons.tsx
@@ -17,60 +17,61 @@ limitations under the License.
 */
 
 import React from "react";
-import PropTypes from "prop-types";
 import { _t } from '../../../languageHandler';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
+interface IProps {
+    // The primary button which is styled differently and has default focus.
+    primaryButton: React.ReactNode;
+
+    // A node to insert into the cancel button instead of default "Cancel"
+    cancelButton?: React.ReactNode;
+
+    // If true, make the primary button a form submit button (input type="submit")
+    primaryIsSubmit?: boolean;
+
+    // onClick handler for the primary button.
+    onPrimaryButtonClick?: (ev: React.MouseEvent) => void;
+
+    // should there be a cancel button? default: true
+    hasCancel?: boolean;
+
+    // The class of the cancel button, only used if a cancel button is
+    // enabled
+    cancelButtonClass?: string;
+
+    // onClick handler for the cancel button.
+    onCancel?: (...args: any[]) => void;
+
+    focus?: boolean;
+
+    // disables the primary and cancel buttons
+    disabled?: boolean;
+
+    // disables only the primary button
+    primaryDisabled?: boolean;
+
+    // something to stick next to the buttons, optionally
+    additive?: React.ReactNode;
+
+    primaryButtonClass?: string;
+}
+
 /**
  * Basic container for buttons in modal dialogs.
  */
 @replaceableComponent("views.elements.DialogButtons")
-export default class DialogButtons extends React.Component {
-    static propTypes = {
-        // The primary button which is styled differently and has default focus.
-        primaryButton: PropTypes.node.isRequired,
-
-        // A node to insert into the cancel button instead of default "Cancel"
-        cancelButton: PropTypes.node,
-
-        // If true, make the primary button a form submit button (input type="submit")
-        primaryIsSubmit: PropTypes.bool,
-
-        // onClick handler for the primary button.
-        onPrimaryButtonClick: PropTypes.func,
-
-        // should there be a cancel button? default: true
-        hasCancel: PropTypes.bool,
-
-        // The class of the cancel button, only used if a cancel button is
-        // enabled
-        cancelButtonClass: PropTypes.node,
-
-        // onClick handler for the cancel button.
-        onCancel: PropTypes.func,
-
-        focus: PropTypes.bool,
-
-        // disables the primary and cancel buttons
-        disabled: PropTypes.bool,
-
-        // disables only the primary button
-        primaryDisabled: PropTypes.bool,
-
-        // something to stick next to the buttons, optionally
-        additive: PropTypes.element,
-    };
-
-    static defaultProps = {
+export default class DialogButtons extends React.Component<IProps> {
+    public static defaultProps: Partial<IProps> = {
         hasCancel: true,
         disabled: false,
     };
 
-    _onCancelClick = () => {
-        this.props.onCancel();
+    private onCancelClick = (event: React.MouseEvent): void => {
+        this.props.onCancel(event);
     };
 
-    render() {
+    public render(): JSX.Element {
         let primaryButtonClassName = "mx_Dialog_primary";
         if (this.props.primaryButtonClass) {
             primaryButtonClassName += " " + this.props.primaryButtonClass;
@@ -82,7 +83,7 @@ export default class DialogButtons extends React.Component {
                 // important: the default type is 'submit' and this button comes before the
                 // primary in the DOM so will get form submissions unless we make it not a submit.
                 type="button"
-                onClick={this._onCancelClick}
+                onClick={this.onCancelClick}
                 className={this.props.cancelButtonClass}
                 disabled={this.props.disabled}
             >
diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.tsx
similarity index 63%
rename from src/components/views/elements/DirectorySearchBox.js
rename to src/components/views/elements/DirectorySearchBox.tsx
index 11b1ed2cd2..07407af622 100644
--- a/src/components/views/elements/DirectorySearchBox.js
+++ b/src/components/views/elements/DirectorySearchBox.tsx
@@ -14,71 +14,73 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
+import React, { ChangeEvent, createRef } from 'react';
 import { _t } from '../../../languageHandler';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import AccessibleButton from "./AccessibleButton";
+
+interface IProps {
+    className?: string;
+    onChange?: (value: string) => void;
+    onClear?: () => void;
+    onJoinClick?: (value: string) => void;
+    placeholder?: string;
+    showJoinButton?: boolean;
+    initialText?: string;
+}
+
+interface IState {
+    value: string;
+}
 
 @replaceableComponent("views.elements.DirectorySearchBox")
-export default class DirectorySearchBox extends React.Component {
-    constructor(props) {
-        super(props);
-        this._collectInput = this._collectInput.bind(this);
-        this._onClearClick = this._onClearClick.bind(this);
-        this._onChange = this._onChange.bind(this);
-        this._onKeyUp = this._onKeyUp.bind(this);
-        this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
+export default class DirectorySearchBox extends React.Component<IProps, IState> {
+    private input = createRef<HTMLInputElement>();
 
-        this.input = null;
+    constructor(props: IProps) {
+        super(props);
 
         this.state = {
             value: this.props.initialText || '',
         };
     }
 
-    _collectInput(e) {
-        this.input = e;
-    }
-
-    _onClearClick() {
+    private onClearClick = (): void => {
         this.setState({ value: '' });
 
-        if (this.input) {
-            this.input.focus();
+        if (this.input.current) {
+            this.input.current.focus();
 
             if (this.props.onClear) {
                 this.props.onClear();
             }
         }
-    }
+    };
 
-    _onChange(ev) {
-        if (!this.input) return;
+    private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
+        if (!this.input.current) return;
         this.setState({ value: ev.target.value });
 
         if (this.props.onChange) {
             this.props.onChange(ev.target.value);
         }
-    }
+    };
 
-    _onKeyUp(ev) {
+    private onKeyUp = (ev: React.KeyboardEvent): void => {
         if (ev.key == 'Enter' && this.props.showJoinButton) {
             if (this.props.onJoinClick) {
                 this.props.onJoinClick(this.state.value);
             }
         }
-    }
+    };
 
-    _onJoinButtonClick() {
+    private onJoinButtonClick = (): void => {
         if (this.props.onJoinClick) {
             this.props.onJoinClick(this.state.value);
         }
-    }
-
-    render() {
-        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
+    };
 
+    public render(): JSX.Element {
         const searchboxClasses = {
             mx_DirectorySearchBox: true,
         };
@@ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
         let joinButton;
         if (this.props.showJoinButton) {
             joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
-                onClick={this._onJoinButtonClick}
+                onClick={this.onJoinButtonClick}
             >{ _t("Join") }</AccessibleButton>;
         }
 
@@ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component {
                 name="dirsearch"
                 value={this.state.value}
                 className="mx_textinput_icon mx_textinput_search"
-                ref={this._collectInput}
-                onChange={this._onChange}
-                onKeyUp={this._onKeyUp}
+                ref={this.input}
+                onChange={this.onChange}
+                onKeyUp={this.onKeyUp}
                 placeholder={this.props.placeholder}
                 autoFocus
             />
             { joinButton }
-            <AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
+            <AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this.onClearClick} />
         </div>;
     }
 }
 
-DirectorySearchBox.propTypes = {
-    className: PropTypes.string,
-    onChange: PropTypes.func,
-    onClear: PropTypes.func,
-    onJoinClick: PropTypes.func,
-    placeholder: PropTypes.string,
-    showJoinButton: PropTypes.bool,
-    initialText: PropTypes.string,
-};
diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.tsx
similarity index 62%
rename from src/components/views/elements/EditableText.js
rename to src/components/views/elements/EditableText.tsx
index 6dbc8b8771..b3ff8ee245 100644
--- a/src/components/views/elements/EditableText.js
+++ b/src/components/views/elements/EditableText.tsx
@@ -16,33 +16,42 @@ limitations under the License.
 */
 
 import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
 import { Key } from "../../../Keyboard";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
+enum Phases {
+    Display = "display",
+    Edit = "edit",
+}
+
+interface IProps {
+    onValueChanged?: (value: string, shouldSubmit: boolean) => void;
+    initialValue?: string;
+    label?: string;
+    placeholder?: string;
+    className?: string;
+    labelClassName?: string;
+    placeholderClassName?: string;
+    // Overrides blurToSubmit if true
+    blurToCancel?: boolean;
+    // Will cause onValueChanged(value, true) to fire on blur
+    blurToSubmit?: boolean;
+    editable?: boolean;
+}
+
+interface IState {
+    phase: Phases;
+}
+
 @replaceableComponent("views.elements.EditableText")
-export default class EditableText extends React.Component {
-    static propTypes = {
-        onValueChanged: PropTypes.func,
-        initialValue: PropTypes.string,
-        label: PropTypes.string,
-        placeholder: PropTypes.string,
-        className: PropTypes.string,
-        labelClassName: PropTypes.string,
-        placeholderClassName: PropTypes.string,
-        // Overrides blurToSubmit if true
-        blurToCancel: PropTypes.bool,
-        // Will cause onValueChanged(value, true) to fire on blur
-        blurToSubmit: PropTypes.bool,
-        editable: PropTypes.bool,
-    };
+export default class EditableText extends React.Component<IProps, IState> {
+    // we track value as an JS object field rather than in React state
+    // as React doesn't play nice with contentEditable.
+    public value = '';
+    private placeholder = false;
+    private editableDiv = createRef<HTMLDivElement>();
 
-    static Phases = {
-        Display: "display",
-        Edit: "edit",
-    };
-
-    static defaultProps = {
+    public static defaultProps: Partial<IProps> = {
         onValueChanged() {},
         initialValue: '',
         label: '',
@@ -53,81 +62,61 @@ export default class EditableText extends React.Component {
         blurToSubmit: false,
     };
 
-    constructor(props) {
+    constructor(props: IProps) {
         super(props);
 
-        // we track value as an JS object field rather than in React state
-        // as React doesn't play nice with contentEditable.
-        this.value = '';
-        this.placeholder = false;
-
-        this._editable_div = createRef();
+        this.state = {
+            phase: Phases.Display,
+        };
     }
 
-    state = {
-        phase: EditableText.Phases.Display,
-    };
-
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    // eslint-disable-next-line camelcase
-    UNSAFE_componentWillReceiveProps(nextProps) {
+    // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
+    public UNSAFE_componentWillReceiveProps(nextProps: IProps): void {
         if (nextProps.initialValue !== this.props.initialValue) {
             this.value = nextProps.initialValue;
-            if (this._editable_div.current) {
+            if (this.editableDiv.current) {
                 this.showPlaceholder(!this.value);
             }
         }
     }
 
-    componentDidMount() {
+    public componentDidMount(): void {
         this.value = this.props.initialValue;
-        if (this._editable_div.current) {
+        if (this.editableDiv.current) {
             this.showPlaceholder(!this.value);
         }
     }
 
-    showPlaceholder = show => {
+    private showPlaceholder = (show: boolean): void => {
         if (show) {
-            this._editable_div.current.textContent = this.props.placeholder;
-            this._editable_div.current.setAttribute("class", this.props.className
+            this.editableDiv.current.textContent = this.props.placeholder;
+            this.editableDiv.current.setAttribute("class", this.props.className
                 + " " + this.props.placeholderClassName);
             this.placeholder = true;
             this.value = '';
         } else {
-            this._editable_div.current.textContent = this.value;
-            this._editable_div.current.setAttribute("class", this.props.className);
+            this.editableDiv.current.textContent = this.value;
+            this.editableDiv.current.setAttribute("class", this.props.className);
             this.placeholder = false;
         }
     };
 
-    getValue = () => this.value;
-
-    setValue = value => {
-        this.value = value;
-        this.showPlaceholder(!this.value);
-    };
-
-    edit = () => {
+    private cancelEdit = (): void => {
         this.setState({
-            phase: EditableText.Phases.Edit,
-        });
-    };
-
-    cancelEdit = () => {
-        this.setState({
-            phase: EditableText.Phases.Display,
+            phase: Phases.Display,
         });
         this.value = this.props.initialValue;
         this.showPlaceholder(!this.value);
         this.onValueChanged(false);
-        this._editable_div.current.blur();
+        this.editableDiv.current.blur();
     };
 
-    onValueChanged = shouldSubmit => {
+    private onValueChanged = (shouldSubmit: boolean): void => {
         this.props.onValueChanged(this.value, shouldSubmit);
     };
 
-    onKeyDown = ev => {
+    private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
         // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
 
         if (this.placeholder) {
@@ -142,13 +131,13 @@ export default class EditableText extends React.Component {
         // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
     };
 
-    onKeyUp = ev => {
+    private onKeyUp = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
         // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
 
-        if (!ev.target.textContent) {
+        if (!(ev.target as HTMLDivElement).textContent) {
             this.showPlaceholder(true);
         } else if (!this.placeholder) {
-            this.value = ev.target.textContent;
+            this.value = (ev.target as HTMLDivElement).textContent;
         }
 
         if (ev.key === Key.ENTER) {
@@ -160,22 +149,22 @@ export default class EditableText extends React.Component {
         // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
     };
 
-    onClickDiv = ev => {
+    private onClickDiv = (): void => {
         if (!this.props.editable) return;
 
         this.setState({
-            phase: EditableText.Phases.Edit,
+            phase: Phases.Edit,
         });
     };
 
-    onFocus = ev => {
+    private onFocus = (ev: React.FocusEvent<HTMLDivElement>): void => {
         //ev.target.setSelectionRange(0, ev.target.textContent.length);
 
         const node = ev.target.childNodes[0];
         if (node) {
             const range = document.createRange();
             range.setStart(node, 0);
-            range.setEnd(node, node.length);
+            range.setEnd(node, ev.target.childNodes.length);
 
             const sel = window.getSelection();
             sel.removeAllRanges();
@@ -183,11 +172,15 @@ export default class EditableText extends React.Component {
         }
     };
 
-    onFinish = (ev, shouldSubmit) => {
+    private onFinish = (
+        ev: React.KeyboardEvent<HTMLDivElement> | React.FocusEvent<HTMLDivElement>,
+        shouldSubmit?: boolean,
+    ): void => {
+        // eslint-disable-next-line @typescript-eslint/no-this-alias
         const self = this;
-        const submit = (ev.key === Key.ENTER) || shouldSubmit;
+        const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit;
         this.setState({
-            phase: EditableText.Phases.Display,
+            phase: Phases.Display,
         }, () => {
             if (this.value !== this.props.initialValue) {
                 self.onValueChanged(submit);
@@ -195,7 +188,7 @@ export default class EditableText extends React.Component {
         });
     };
 
-    onBlur = ev => {
+    private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
         const sel = window.getSelection();
         sel.removeAllRanges();
 
@@ -208,11 +201,11 @@ export default class EditableText extends React.Component {
         this.showPlaceholder(!this.value);
     };
 
-    render() {
+    public render(): JSX.Element {
         const { className, editable, initialValue, label, labelClassName } = this.props;
         let editableEl;
 
-        if (!editable || (this.state.phase === EditableText.Phases.Display &&
+        if (!editable || (this.state.phase === Phases.Display &&
             (label || labelClassName) && !this.value)
         ) {
             // show the label
@@ -222,7 +215,7 @@ export default class EditableText extends React.Component {
         } else {
             // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
             editableEl = <div
-                ref={this._editable_div}
+                ref={this.editableDiv}
                 contentEditable={true}
                 className={className}
                 onKeyDown={this.onKeyDown}
diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.tsx
similarity index 62%
rename from src/components/views/elements/EditableTextContainer.js
rename to src/components/views/elements/EditableTextContainer.tsx
index 5778446355..b4689b90db 100644
--- a/src/components/views/elements/EditableTextContainer.js
+++ b/src/components/views/elements/EditableTextContainer.tsx
@@ -15,9 +15,34 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import Spinner from "./Spinner";
+import EditableText from "./EditableText";
+
+interface IProps {
+    /* callback to retrieve the initial value. */
+    getInitialValue?: () => Promise<string>;
+
+    /* initial value; used if getInitialValue is not given */
+    initialValue?: string;
+
+    /* placeholder text to use when the value is empty (and not being
+     * edited) */
+    placeholder?: string;
+
+    /* callback to update the value. Called with a single argument: the new
+     * value. */
+    onSubmit?: (value: string) => Promise<{} | void>;
+
+    /* should the input submit when focus is lost? */
+    blurToSubmit?: boolean;
+}
+
+interface IState {
+    busy: boolean;
+    errorString: string;
+    value: string;
+}
 
 /**
  * A component which wraps an EditableText, with a spinner while updates take
@@ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
  * taken from the 'initialValue' property.
  */
 @replaceableComponent("views.elements.EditableTextContainer")
-export default class EditableTextContainer extends React.Component {
-    constructor(props) {
+export default class EditableTextContainer extends React.Component<IProps, IState> {
+    private unmounted = false;
+    public static defaultProps: Partial<IProps> = {
+        initialValue: "",
+        placeholder: "",
+        blurToSubmit: false,
+        onSubmit: () => { return Promise.resolve(); },
+    };
+
+    constructor(props: IProps) {
         super(props);
 
-        this._unmounted = false;
         this.state = {
             busy: false,
             errorString: null,
             value: props.initialValue,
         };
-        this._onValueChanged = this._onValueChanged.bind(this);
     }
 
-    componentDidMount() {
-        if (this.props.getInitialValue === undefined) {
-            // use whatever was given in the initialValue property.
-            return;
-        }
+    public async componentDidMount(): Promise<void> {
+        // use whatever was given in the initialValue property.
+        if (this.props.getInitialValue === undefined) return;
 
         this.setState({ busy: true });
-
-        this.props.getInitialValue().then(
-            (result) => {
-                if (this._unmounted) { return; }
-                this.setState({
-                    busy: false,
-                    value: result,
-                });
-            },
-            (error) => {
-                if (this._unmounted) { return; }
-                this.setState({
-                    errorString: error.toString(),
-                    busy: false,
-                });
-            },
-        );
+        try {
+            const initialValue = await this.props.getInitialValue();
+            if (this.unmounted) return;
+            this.setState({
+                busy: false,
+                value: initialValue,
+            });
+        } catch (error) {
+            if (this.unmounted) return;
+            this.setState({
+                errorString: error.toString(),
+                busy: false,
+            });
+        }
     }
 
-    componentWillUnmount() {
-        this._unmounted = true;
+    public componentWillUnmount(): void {
+        this.unmounted = true;
     }
 
-    _onValueChanged(value, shouldSubmit) {
+    private onValueChanged = (value: string, shouldSubmit: boolean): void => {
         if (!shouldSubmit) {
             return;
         }
@@ -86,38 +112,36 @@ export default class EditableTextContainer extends React.Component {
 
         this.props.onSubmit(value).then(
             () => {
-                if (this._unmounted) { return; }
+                if (this.unmounted) { return; }
                 this.setState({
                     busy: false,
                     value: value,
                 });
             },
             (error) => {
-                if (this._unmounted) { return; }
+                if (this.unmounted) { return; }
                 this.setState({
                     errorString: error.toString(),
                     busy: false,
                 });
             },
         );
-    }
+    };
 
-    render() {
+    public render(): JSX.Element {
         if (this.state.busy) {
-            const Loader = sdk.getComponent("elements.Spinner");
             return (
-                <Loader />
+                <Spinner />
             );
         } else if (this.state.errorString) {
             return (
                 <div className="error">{ this.state.errorString }</div>
             );
         } else {
-            const EditableText = sdk.getComponent('elements.EditableText');
             return (
                 <EditableText initialValue={this.state.value}
                     placeholder={this.props.placeholder}
-                    onValueChanged={this._onValueChanged}
+                    onValueChanged={this.onValueChanged}
                     blurToSubmit={this.props.blurToSubmit}
                 />
             );
@@ -125,28 +149,3 @@ export default class EditableTextContainer extends React.Component {
     }
 }
 
-EditableTextContainer.propTypes = {
-    /* callback to retrieve the initial value. */
-    getInitialValue: PropTypes.func,
-
-    /* initial value; used if getInitialValue is not given */
-    initialValue: PropTypes.string,
-
-    /* placeholder text to use when the value is empty (and not being
-     * edited) */
-    placeholder: PropTypes.string,
-
-    /* callback to update the value. Called with a single argument: the new
-     * value. */
-    onSubmit: PropTypes.func,
-
-    /* should the input submit when focus is lost? */
-    blurToSubmit: PropTypes.bool,
-};
-
-EditableTextContainer.defaultProps = {
-    initialValue: "",
-    placeholder: "",
-    blurToSubmit: false,
-    onSubmit: function(v) {return Promise.resolve(); },
-};
diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.tsx
similarity index 84%
rename from src/components/views/elements/LanguageDropdown.js
rename to src/components/views/elements/LanguageDropdown.tsx
index 3f17a78629..c6c52ee4e8 100644
--- a/src/components/views/elements/LanguageDropdown.js
+++ b/src/components/views/elements/LanguageDropdown.tsx
@@ -16,13 +16,13 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import * as sdk from '../../../index';
 import * as languageHandler from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import { _t } from "../../../languageHandler";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import Spinner from "./Spinner";
+import Dropdown from "./Dropdown";
 
 function languageMatchesSearchQuery(query, language) {
     if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@@ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) {
     return false;
 }
 
+interface IProps {
+    className?: string;
+    onOptionChange: (language: string) => void;
+    value?: string;
+    disabled?: boolean;
+}
+
+interface IState {
+    searchQuery: string;
+    langs: string[];
+}
+
 @replaceableComponent("views.elements.LanguageDropdown")
-export default class LanguageDropdown extends React.Component {
-    constructor(props) {
+export default class LanguageDropdown extends React.Component<IProps, IState> {
+    constructor(props: IProps) {
         super(props);
-        this._onSearchChange = this._onSearchChange.bind(this);
 
         this.state = {
             searchQuery: '',
@@ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component {
         };
     }
 
-    componentDidMount() {
+    public componentDidMount(): void {
         languageHandler.getAllLanguagesFromJson().then((langs) => {
             langs.sort(function(a, b) {
                 if (a.label < b.label) return -1;
@@ -63,20 +74,17 @@ export default class LanguageDropdown extends React.Component {
         }
     }
 
-    _onSearchChange(search) {
+    private onSearchChange = (search: string): void => {
         this.setState({
             searchQuery: search,
         });
-    }
+    };
 
-    render() {
+    public render(): JSX.Element {
         if (this.state.langs === null) {
-            const Spinner = sdk.getComponent('elements.Spinner');
             return <Spinner />;
         }
 
-        const Dropdown = sdk.getComponent('elements.Dropdown');
-
         let displayedLanguages;
         if (this.state.searchQuery) {
             displayedLanguages = this.state.langs.filter((lang) => {
@@ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
             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")}
@@ -118,8 +126,3 @@ export default class LanguageDropdown extends React.Component {
     }
 }
 
-LanguageDropdown.propTypes = {
-    className: PropTypes.string,
-    onOptionChange: PropTypes.func.isRequired,
-    value: PropTypes.string,
-};
diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.tsx
similarity index 80%
rename from src/components/views/elements/LazyRenderList.js
rename to src/components/views/elements/LazyRenderList.tsx
index 070d9bcc8d..54c76f27a7 100644
--- a/src/components/views/elements/LazyRenderList.js
+++ b/src/components/views/elements/LazyRenderList.tsx
@@ -15,17 +15,16 @@ limitations under the License.
 */
 
 import React from "react";
-import PropTypes from 'prop-types';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
 class ItemRange {
-    constructor(topCount, renderCount, bottomCount) {
-        this.topCount = topCount;
-        this.renderCount = renderCount;
-        this.bottomCount = bottomCount;
-    }
+    constructor(
+        public topCount: number,
+        public renderCount: number,
+        public bottomCount: number,
+    ) { }
 
-    contains(range) {
+    public contains(range: ItemRange): boolean {
         // don't contain empty ranges
         // as it will prevent clearing the list
         // once it is scrolled far enough out of view
@@ -36,7 +35,7 @@ class ItemRange {
             (range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
     }
 
-    expand(amount) {
+    public expand(amount: number): ItemRange {
         // don't expand ranges that won't render anything
         if (this.renderCount === 0) {
             return this;
@@ -51,20 +50,55 @@ class ItemRange {
         );
     }
 
-    totalSize() {
+    public totalSize(): number {
         return this.topCount + this.renderCount + this.bottomCount;
     }
 }
 
+interface IProps<T> {
+    // height in pixels of the component returned by `renderItem`
+    itemHeight: number;
+    // function to turn an element of `items` into a react component
+    renderItem: (item: T) => JSX.Element;
+    // scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
+    scrollTop: number;
+    // the height of the viewport this content is scrolled in
+    height: number;
+    // all items for the list. These should not be react components, see `renderItem`.
+    items?: T[];
+    // the amount of items to scroll before causing a rerender,
+    // should typically be less than `overflowItems` unless applying
+    // margins in the parent component when using multiple LazyRenderList in one viewport.
+    // use 0 to only rerender when items will come into view.
+    overflowMargin?: number;
+    // the amount of items to add at the top and bottom to render,
+    // so not every scroll of causes a rerender.
+    overflowItems?: number;
+
+    element?: string;
+    className?: string;
+}
+
+interface IState {
+    renderRange: ItemRange;
+}
+
 @replaceableComponent("views.elements.LazyRenderList")
-export default class LazyRenderList extends React.Component {
-    constructor(props) {
+export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> {
+    public static defaultProps: Partial<IProps<unknown>> = {
+        overflowItems: 20,
+        overflowMargin: 5,
+    };
+
+    constructor(props: IProps<T>) {
         super(props);
 
-        this.state = {};
+        this.state = {
+            renderRange: null,
+        };
     }
 
-    static getDerivedStateFromProps(props, state) {
+    public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> {
         const range = LazyRenderList.getVisibleRangeFromProps(props);
         const intersectRange = range.expand(props.overflowMargin);
         const renderRange = range.expand(props.overflowItems);
@@ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component {
         return null;
     }
 
-    static getVisibleRangeFromProps(props) {
+    private static getVisibleRangeFromProps(props: IProps<unknown>): ItemRange {
         const { items, itemHeight, scrollTop, height } = props;
         const length = items ? items.length : 0;
         const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
@@ -88,7 +122,7 @@ export default class LazyRenderList extends React.Component {
         return new ItemRange(topCount, renderCount, bottomCount);
     }
 
-    render() {
+    public render(): JSX.Element {
         const { itemHeight, items, renderItem } = this.props;
         const { renderRange } = this.state;
         const { topCount, renderCount, bottomCount } = renderRange;
@@ -109,28 +143,3 @@ export default class LazyRenderList extends React.Component {
     }
 }
 
-LazyRenderList.defaultProps = {
-    overflowItems: 20,
-    overflowMargin: 5,
-};
-
-LazyRenderList.propTypes = {
-    // height in pixels of the component returned by `renderItem`
-    itemHeight: PropTypes.number.isRequired,
-    // function to turn an element of `items` into a react component
-    renderItem: PropTypes.func.isRequired,
-    // scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
-    scrollTop: PropTypes.number.isRequired,
-    // the height of the viewport this content is scrolled in
-    height: PropTypes.number.isRequired,
-    // all items for the list. These should not be react components, see `renderItem`.
-    items: PropTypes.array,
-    // the amount of items to scroll before causing a rerender,
-    // should typically be less than `overflowItems` unless applying
-    // margins in the parent component when using multiple LazyRenderList in one viewport.
-    // use 0 to only rerender when items will come into view.
-    overflowMargin: PropTypes.number,
-    // the amount of items to add at the top and bottom to render,
-    // so not every scroll of causes a rerender.
-    overflowItems: PropTypes.number,
-};
diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx
index 0722cb872a..4eb0177fef 100644
--- a/src/components/views/elements/MemberEventListSummary.tsx
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -135,7 +135,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
 
             const desc = formatCommaSeparatedList(descs);
 
-            return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
+            return _t('%(nameList)s %(transitionList)s', { nameList, transitionList: desc });
         });
 
         if (!summaries) {
diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.tsx
similarity index 69%
rename from src/components/views/elements/PersistedElement.js
rename to src/components/views/elements/PersistedElement.tsx
index 03aa9e0d6d..d013091803 100644
--- a/src/components/views/elements/PersistedElement.js
+++ b/src/components/views/elements/PersistedElement.tsx
@@ -16,25 +16,26 @@ limitations under the License.
 
 import React from 'react';
 import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
 import { throttle } from "lodash";
-import ResizeObserver from 'resize-observer-polyfill';
 
 import dis from '../../../dispatcher/dispatcher';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { ActionPayload } from "../../../dispatcher/payloads";
+
+export const getPersistKey = (appId: string) => 'widget_' + appId;
 
 // Shamelessly ripped off Modal.js.  There's probably a better way
 // of doing reusable widgets like dialog boxes & menus where we go and
 // pass in a custom control as the actual body.
 
-function getContainer(containerId) {
-    return document.getElementById(containerId);
+function getContainer(containerId: string): HTMLDivElement {
+    return document.getElementById(containerId) as HTMLDivElement;
 }
 
-function getOrCreateContainer(containerId) {
+function getOrCreateContainer(containerId: string): HTMLDivElement {
     let container = getContainer(containerId);
 
     if (!container) {
@@ -46,7 +47,19 @@ function getOrCreateContainer(containerId) {
     return container;
 }
 
-/*
+interface IProps {
+    // Unique identifier for this PersistedElement instance
+    // Any PersistedElements with the same persistKey will use
+    // the same DOM container.
+    persistKey: string;
+
+    // z-index for the element. Defaults to 9.
+    zIndex?: number;
+
+    style?: React.StyleHTMLAttributes<HTMLDivElement>;
+}
+
+/**
  * Class of component that renders its children in a separate ReactDOM virtual tree
  * in a container element appended to document.body.
  *
@@ -58,42 +71,33 @@ function getOrCreateContainer(containerId) {
  * bounding rect as the parent of PE.
  */
 @replaceableComponent("views.elements.PersistedElement")
-export default class PersistedElement extends React.Component {
-    static propTypes = {
-        // Unique identifier for this PersistedElement instance
-        // Any PersistedElements with the same persistKey will use
-        // the same DOM container.
-        persistKey: PropTypes.string.isRequired,
+export default class PersistedElement extends React.Component<IProps> {
+    private resizeObserver: ResizeObserver;
+    private dispatcherRef: string;
+    private childContainer: HTMLDivElement;
+    private child: HTMLDivElement;
 
-        // z-index for the element. Defaults to 9.
-        zIndex: PropTypes.number,
-    };
+    constructor(props: IProps) {
+        super(props);
 
-    constructor() {
-        super();
-        this.collectChildContainer = this.collectChildContainer.bind(this);
-        this.collectChild = this.collectChild.bind(this);
-        this._repositionChild = this._repositionChild.bind(this);
-        this._onAction = this._onAction.bind(this);
-
-        this.resizeObserver = new ResizeObserver(this._repositionChild);
+        this.resizeObserver = new ResizeObserver(this.repositionChild);
         // Annoyingly, a resize observer is insufficient, since we also care
         // about when the element moves on the screen without changing its
         // dimensions. Doesn't look like there's a ResizeObserver equivalent
         // for this, so we bodge it by listening for document resize and
         // the timeline_resize action.
-        window.addEventListener('resize', this._repositionChild);
-        this._dispatcherRef = dis.register(this._onAction);
+        window.addEventListener('resize', this.repositionChild);
+        this.dispatcherRef = dis.register(this.onAction);
     }
 
     /**
      * Removes the DOM elements created when a PersistedElement with the given
      * persistKey was mounted. The DOM elements will be re-added if another
-     * PeristedElement is mounted in the future.
+     * PersistedElement is mounted in the future.
      *
      * @param {string} persistKey Key used to uniquely identify this PersistedElement
      */
-    static destroyElement(persistKey) {
+    public static destroyElement(persistKey: string): void {
         const container = getContainer('mx_persistedElement_' + persistKey);
         if (container) {
             container.remove();
@@ -104,7 +108,7 @@ export default class PersistedElement extends React.Component {
         return Boolean(getContainer('mx_persistedElement_' + persistKey));
     }
 
-    collectChildContainer(ref) {
+    private collectChildContainer = (ref: HTMLDivElement): void => {
         if (this.childContainer) {
             this.resizeObserver.unobserve(this.childContainer);
         }
@@ -112,48 +116,48 @@ export default class PersistedElement extends React.Component {
         if (ref) {
             this.resizeObserver.observe(ref);
         }
-    }
+    };
 
-    collectChild(ref) {
+    private collectChild = (ref: HTMLDivElement): void => {
         this.child = ref;
         this.updateChild();
-    }
+    };
 
-    componentDidMount() {
+    public componentDidMount(): void {
         this.updateChild();
         this.renderApp();
     }
 
-    componentDidUpdate() {
+    public componentDidUpdate(): void {
         this.updateChild();
         this.renderApp();
     }
 
-    componentWillUnmount() {
+    public componentWillUnmount(): void {
         this.updateChildVisibility(this.child, false);
         this.resizeObserver.disconnect();
-        window.removeEventListener('resize', this._repositionChild);
-        dis.unregister(this._dispatcherRef);
+        window.removeEventListener('resize', this.repositionChild);
+        dis.unregister(this.dispatcherRef);
     }
 
-    _onAction(payload) {
+    private onAction = (payload: ActionPayload): void => {
         if (payload.action === 'timeline_resize') {
-            this._repositionChild();
+            this.repositionChild();
         } else if (payload.action === 'logout') {
             PersistedElement.destroyElement(this.props.persistKey);
         }
-    }
+    };
 
-    _repositionChild() {
+    private repositionChild = (): void => {
         this.updateChildPosition(this.child, this.childContainer);
-    }
+    };
 
-    updateChild() {
+    private updateChild(): void {
         this.updateChildPosition(this.child, this.childContainer);
         this.updateChildVisibility(this.child, true);
     }
 
-    renderApp() {
+    private renderApp(): void {
         const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
             <div ref={this.collectChild} style={this.props.style}>
                 { this.props.children }
@@ -163,12 +167,12 @@ export default class PersistedElement extends React.Component {
         ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
     }
 
-    updateChildVisibility(child, visible) {
+    private updateChildVisibility(child: HTMLDivElement, visible: boolean): void {
         if (!child) return;
         child.style.display = visible ? 'block' : 'none';
     }
 
-    updateChildPosition = throttle((child, parent) => {
+    private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => {
         if (!child || !parent) return;
 
         const parentRect = parent.getBoundingClientRect();
@@ -182,9 +186,8 @@ export default class PersistedElement extends React.Component {
         });
     }, 100, { trailing: true, leading: true });
 
-    render() {
+    public render(): JSX.Element {
         return <div ref={this.collectChildContainer} />;
     }
 }
 
-export const getPersistKey = (appId) => 'widget_' + appId;
diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.tsx
similarity index 72%
rename from src/components/views/elements/PersistentApp.js
rename to src/components/views/elements/PersistentApp.tsx
index 763ab63487..1f911659e2 100644
--- a/src/components/views/elements/PersistentApp.js
+++ b/src/components/views/elements/PersistentApp.tsx
@@ -19,57 +19,70 @@ import React from 'react';
 import RoomViewStore from '../../../stores/RoomViewStore';
 import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
 import WidgetUtils from '../../../utils/WidgetUtils';
-import * as sdk from '../../../index';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { EventSubscription } from 'fbemitter';
+import AppTile from "./AppTile";
+import { Room } from "matrix-js-sdk/src/models/room";
+
+interface IState {
+    roomId: string;
+    persistentWidgetId: string;
+}
 
 @replaceableComponent("views.elements.PersistentApp")
-export default class PersistentApp extends React.Component {
-    state = {
-        roomId: RoomViewStore.getRoomId(),
-        persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
-    };
+export default class PersistentApp extends React.Component<{}, IState> {
+    private roomStoreToken: EventSubscription;
 
-    componentDidMount() {
-        this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
-        ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
-        MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
+    constructor() {
+        super({});
+
+        this.state = {
+            roomId: RoomViewStore.getRoomId(),
+            persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
+        };
     }
 
-    componentWillUnmount() {
-        if (this._roomStoreToken) {
-            this._roomStoreToken.remove();
+    public componentDidMount(): void {
+        this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
+        ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate);
+        MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
+    }
+
+    public componentWillUnmount(): void {
+        if (this.roomStoreToken) {
+            this.roomStoreToken.remove();
         }
-        ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
+        ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate);
         if (MatrixClientPeg.get()) {
-            MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership);
+            MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
         }
     }
 
-    _onRoomViewStoreUpdate = payload => {
+    private onRoomViewStoreUpdate = (): void => {
         if (RoomViewStore.getRoomId() === this.state.roomId) return;
         this.setState({
             roomId: RoomViewStore.getRoomId(),
         });
     };
 
-    _onActiveWidgetStoreUpdate = () => {
+    private onActiveWidgetStoreUpdate = (): void => {
         this.setState({
             persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
         });
     };
 
-    _onMyMembership = async (room, membership) => {
+    private onMyMembership = async (room: Room, membership: string): Promise<void> => {
         const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
         if (membership !== "join") {
             // we're not in the room anymore - delete
-            if (room.roomId === persistentWidgetInRoomId) {
+            if (room .roomId === persistentWidgetInRoomId) {
                 ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
             }
         }
     };
 
-    render() {
+    public render(): JSX.Element {
         if (this.state.persistentWidgetId) {
             const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
 
@@ -89,7 +102,6 @@ export default class PersistentApp extends React.Component {
                     appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
                     persistentWidgetInRoomId, appEvent.getId(),
                 );
-                const AppTile = sdk.getComponent('elements.AppTile');
                 return <AppTile
                     key={app.id}
                     app={app}
diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.tsx
similarity index 60%
rename from src/components/views/elements/PowerSelector.js
rename to src/components/views/elements/PowerSelector.tsx
index 42386ca5c1..a99812028e 100644
--- a/src/components/views/elements/PowerSelector.js
+++ b/src/components/views/elements/PowerSelector.tsx
@@ -15,40 +15,52 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import * as Roles from '../../../Roles';
 import { _t } from '../../../languageHandler';
 import Field from "./Field";
 import { Key } from "../../../Keyboard";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
+const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
+
+interface IProps {
+    value: number;
+    // The maximum value that can be set with the power selector
+    maxValue: number;
+
+    // Default user power level for the room
+    usersDefault: number;
+
+    // should the user be able to change the value? false by default.
+    disabled?: boolean;
+    onChange?: (value: number, powerLevelKey: string) => void;
+
+    // Optional key to pass as the second argument to `onChange`
+    powerLevelKey?: string;
+
+    // The name to annotate the selector with
+    label?: string;
+}
+
+interface IState {
+    levelRoleMap: {};
+    // List of power levels to show in the drop-down
+    options: number[];
+
+    customValue: number;
+    selectValue: number | string;
+    custom?: boolean;
+    customLevel?: number;
+}
+
 @replaceableComponent("views.elements.PowerSelector")
-export default class PowerSelector extends React.Component {
-    static propTypes = {
-        value: PropTypes.number.isRequired,
-        // The maximum value that can be set with the power selector
-        maxValue: PropTypes.number.isRequired,
-
-        // Default user power level for the room
-        usersDefault: PropTypes.number.isRequired,
-
-        // should the user be able to change the value? false by default.
-        disabled: PropTypes.bool,
-        onChange: PropTypes.func,
-
-        // Optional key to pass as the second argument to `onChange`
-        powerLevelKey: PropTypes.string,
-
-        // The name to annotate the selector with
-        label: PropTypes.string,
-    }
-
-    static defaultProps = {
+export default class PowerSelector extends React.Component<IProps, IState> {
+    public static defaultProps: Partial<IProps> = {
         maxValue: Infinity,
         usersDefault: 0,
     };
 
-    constructor(props) {
+    constructor(props: IProps) {
         super(props);
 
         this.state = {
@@ -62,26 +74,26 @@ export default class PowerSelector extends React.Component {
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
-    // eslint-disable-next-line camelcase
-    UNSAFE_componentWillMount() {
-        this._initStateFromProps(this.props);
+    // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
+    public UNSAFE_componentWillMount(): void {
+        this.initStateFromProps(this.props);
     }
 
-    // eslint-disable-next-line camelcase
-    UNSAFE_componentWillReceiveProps(newProps) {
-        this._initStateFromProps(newProps);
+    // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
+    public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
+        this.initStateFromProps(newProps);
     }
 
-    _initStateFromProps(newProps) {
+    private initStateFromProps(newProps: IProps): void {
         // This needs to be done now because levelRoleMap has translated strings
         const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
         const options = Object.keys(levelRoleMap).filter(level => {
             return (
                 level === undefined ||
-                level <= newProps.maxValue ||
-                level == newProps.value
+                parseInt(level) <= newProps.maxValue ||
+                parseInt(level) == newProps.value
             );
-        });
+        }).map(level => parseInt(level));
 
         const isCustom = levelRoleMap[newProps.value] === undefined;
 
@@ -90,32 +102,33 @@ export default class PowerSelector extends React.Component {
             options,
             custom: isCustom,
             customLevel: newProps.value,
-            selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value,
+            selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
         });
     }
 
-    onSelectChange = event => {
-        const isCustom = event.target.value === "SELECT_VALUE_CUSTOM";
+    private onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
+        const isCustom = event.target.value === CUSTOM_VALUE;
         if (isCustom) {
             this.setState({ custom: true });
         } else {
-            this.props.onChange(event.target.value, this.props.powerLevelKey);
-            this.setState({ selectValue: event.target.value });
+            const powerLevel = parseInt(event.target.value);
+            this.props.onChange(powerLevel, this.props.powerLevelKey);
+            this.setState({ selectValue: powerLevel });
         }
     };
 
-    onCustomChange = event => {
-        this.setState({ customValue: event.target.value });
+    private onCustomChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
+        this.setState({ customValue: parseInt(event.target.value) });
     };
 
-    onCustomBlur = event => {
+    private onCustomBlur = (event: React.FocusEvent): void => {
         event.preventDefault();
         event.stopPropagation();
 
-        this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
+        this.props.onChange(this.state.customValue, this.props.powerLevelKey);
     };
 
-    onCustomKeyDown = event => {
+    private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
         if (event.key === Key.ENTER) {
             event.preventDefault();
             event.stopPropagation();
@@ -125,11 +138,11 @@ export default class PowerSelector extends React.Component {
             // raising a dialog which causes a blur which causes a dialog which causes a blur and
             // so on. By not causing the onChange to be called here, we avoid the loop because we
             // handle the onBlur safely.
-            event.target.blur();
+            (event.target as HTMLInputElement).blur();
         }
     };
 
-    render() {
+    public render(): JSX.Element {
         let picker;
         const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
         if (this.state.custom) {
@@ -147,14 +160,14 @@ export default class PowerSelector extends React.Component {
             );
         } else {
             // Each level must have a definition in this.state.levelRoleMap
-            let options = this.state.options.map((level) => {
+            const options = this.state.options.map((level) => {
                 return {
-                    value: level,
+                    value: String(level),
                     text: Roles.textualPowerLevel(level, this.props.usersDefault),
                 };
             });
-            options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
-            options = options.map((op) => {
+            options.push({ value: CUSTOM_VALUE, text: _t("Custom level") });
+            const optionsElements = options.map((op) => {
                 return <option value={op.value} key={op.value}>{ op.text }</option>;
             });
 
@@ -166,7 +179,7 @@ export default class PowerSelector extends React.Component {
                     value={String(this.state.selectValue)}
                     disabled={this.props.disabled}
                 >
-                    { options }
+                    { optionsElements }
                 </Field>
             );
         }
diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx
index d061d52f46..59c827d5d8 100644
--- a/src/components/views/elements/ReplyThread.tsx
+++ b/src/components/views/elements/ReplyThread.tsx
@@ -88,7 +88,13 @@ export default class ReplyThread extends React.Component<IProps, IState> {
         // could be used here for replies as well... However, the helper
         // currently assumes the relation has a `rel_type`, which older replies
         // do not, so this block is left as-is for now.
-        const mRelatesTo = ev.getWireContent()['m.relates_to'];
+        //
+        // We're prefer ev.getContent() over ev.getWireContent() to make sure
+        // we grab the latest edit with potentially new relations. But we also
+        // can't just rely on ev.getContent() by itself because historically we
+        // still show the reply from the original message even though the edit
+        // event does not include the relation reply.
+        const mRelatesTo = ev.getContent()['m.relates_to'] || ev.getWireContent()['m.relates_to'];
         if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
             const mInReplyTo = mRelatesTo['m.in_reply_to'];
             if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
diff --git a/src/components/views/elements/Spoiler.js b/src/components/views/elements/Spoiler.tsx
similarity index 82%
rename from src/components/views/elements/Spoiler.js
rename to src/components/views/elements/Spoiler.tsx
index 802c6cf841..4779a7d90e 100644
--- a/src/components/views/elements/Spoiler.js
+++ b/src/components/views/elements/Spoiler.tsx
@@ -17,25 +17,34 @@
 import React from 'react';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
+interface IProps {
+    reason?: string;
+    contentHtml: string;
+}
+
+interface IState {
+    visible: boolean;
+}
+
 @replaceableComponent("views.elements.Spoiler")
-export default class Spoiler extends React.Component {
-    constructor(props) {
+export default class Spoiler extends React.Component<IProps, IState> {
+    constructor(props: IProps) {
         super(props);
         this.state = {
             visible: false,
         };
     }
 
-    toggleVisible(e) {
+    private toggleVisible = (e: React.MouseEvent): void => {
         if (!this.state.visible) {
             // we are un-blurring, we don't want this click to propagate to potential child pills
             e.preventDefault();
             e.stopPropagation();
         }
         this.setState({ visible: !this.state.visible });
-    }
+    };
 
-    render() {
+    public render(): JSX.Element {
         const reason = this.props.reason ? (
             <span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
         ) : null;
@@ -43,7 +52,7 @@ export default class Spoiler extends React.Component {
         // as such, we pass the this.props.contentHtml instead and then set the raw
         // HTML content. This is secure as the contents have already been parsed previously
         return (
-            <span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible.bind(this)}>
+            <span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible}>
                 { reason }
                 &nbsp;
                 <span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
diff --git a/src/components/views/elements/SyntaxHighlight.js b/src/components/views/elements/SyntaxHighlight.tsx
similarity index 73%
rename from src/components/views/elements/SyntaxHighlight.js
rename to src/components/views/elements/SyntaxHighlight.tsx
index 2c29f7c989..cd65cddfba 100644
--- a/src/components/views/elements/SyntaxHighlight.js
+++ b/src/components/views/elements/SyntaxHighlight.tsx
@@ -15,40 +15,40 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import { highlightBlock } from 'highlight.js';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
+interface IProps {
+    className?: string;
+    children?: React.ReactNode;
+}
+
 @replaceableComponent("views.elements.SyntaxHighlight")
-export default class SyntaxHighlight extends React.Component {
-    static propTypes = {
-        className: PropTypes.string,
-        children: PropTypes.node,
-    };
+export default class SyntaxHighlight extends React.Component<IProps> {
+    private el: HTMLPreElement = null;
 
-    constructor(props) {
+    constructor(props: IProps) {
         super(props);
-
-        this._ref = this._ref.bind(this);
     }
 
     // componentDidUpdate used here for reusability
-    componentDidUpdate() {
-        if (this._el) highlightBlock(this._el);
+    public componentDidUpdate(): void {
+        if (this.el) highlightBlock(this.el);
     }
 
     // call componentDidUpdate because _ref is fired on initial render
     // which does not fire componentDidUpdate
-    _ref(el) {
-        this._el = el;
+    private ref = (el: HTMLPreElement): void => {
+        this.el = el;
         this.componentDidUpdate();
-    }
+    };
 
-    render() {
+    public render(): JSX.Element {
         const { className, children } = this.props;
 
-        return <pre className={`${className} mx_SyntaxHighlight`} ref={this._ref}>
+        return <pre className={`${className} mx_SyntaxHighlight`} ref={this.ref}>
             <code>{ children }</code>
         </pre>;
     }
 }
+
diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.tsx
similarity index 71%
rename from src/components/views/elements/TextWithTooltip.js
rename to src/components/views/elements/TextWithTooltip.tsx
index 288d33f71b..b7c2477158 100644
--- a/src/components/views/elements/TextWithTooltip.js
+++ b/src/components/views/elements/TextWithTooltip.tsx
@@ -15,42 +15,44 @@
  */
 
 import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import Tooltip from "./Tooltip";
+
+interface IProps {
+    class?: string;
+    tooltipClass?: string;
+    tooltip: React.ReactNode;
+    tooltipProps?: {};
+    onClick?: (ev?: React.MouseEvent) => void;
+}
+
+interface IState {
+    hover: boolean;
+}
 
 @replaceableComponent("views.elements.TextWithTooltip")
-export default class TextWithTooltip extends React.Component {
-    static propTypes = {
-        class: PropTypes.string,
-        tooltipClass: PropTypes.string,
-        tooltip: PropTypes.node.isRequired,
-        tooltipProps: PropTypes.object,
-    };
-
-    constructor() {
-        super();
+export default class TextWithTooltip extends React.Component<IProps, IState> {
+    constructor(props: IProps) {
+        super(props);
 
         this.state = {
             hover: false,
         };
     }
 
-    onMouseOver = () => {
+    private onMouseOver = (): void => {
         this.setState({ hover: true });
     };
 
-    onMouseLeave = () => {
+    private onMouseLeave = (): void => {
         this.setState({ hover: false });
     };
 
-    render() {
-        const Tooltip = sdk.getComponent("elements.Tooltip");
-
+    public render(): JSX.Element {
         const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
 
         return (
-            <span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
+            <span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick} className={className}>
                 { children }
                 { this.state.hover && <Tooltip
                     {...tooltipProps}
diff --git a/src/components/views/elements/crypto/VerificationQRCode.js b/src/components/views/elements/crypto/VerificationQRCode.tsx
similarity index 79%
rename from src/components/views/elements/crypto/VerificationQRCode.js
rename to src/components/views/elements/crypto/VerificationQRCode.tsx
index 76cfb82d35..be9ede59b1 100644
--- a/src/components/views/elements/crypto/VerificationQRCode.js
+++ b/src/components/views/elements/crypto/VerificationQRCode.tsx
@@ -15,20 +15,20 @@ limitations under the License.
 */
 
 import React from "react";
-import PropTypes from "prop-types";
 import { replaceableComponent } from "../../../../utils/replaceableComponent";
 import QRCode from "../QRCode";
+import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode";
+
+interface IProps {
+    qrCodeData: QRCodeData;
+}
 
 @replaceableComponent("views.elements.crypto.VerificationQRCode")
-export default class VerificationQRCode extends React.PureComponent {
-    static propTypes = {
-        qrCodeData: PropTypes.object.isRequired,
-    };
-
-    render() {
+export default class VerificationQRCode extends React.PureComponent<IProps> {
+    public render(): JSX.Element {
         return (
             <QRCode
-                data={[{ data: this.props.qrCodeData.buffer, mode: 'byte' }]}
+                data={[{ data: this.props.qrCodeData.getBuffer(), mode: 'byte' }]}
                 className="mx_VerificationQRCode"
                 width={196} />
         );
diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx
index 7498a49173..8934b2b98f 100644
--- a/src/components/views/messages/ReactionsRowButton.tsx
+++ b/src/components/views/messages/ReactionsRowButton.tsx
@@ -106,31 +106,20 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
         }
 
         const room = this.context.getRoom(mxEvent.getRoomId());
-        let label;
+        let label: string;
         if (room) {
             const senders = [];
             for (const reactionEvent of reactionEvents) {
                 const member = room.getMember(reactionEvent.getSender());
-                const name = member ? member.name : reactionEvent.getSender();
-                senders.push(name);
+                senders.push(member?.name || reactionEvent.getSender());
+            }
+
+            const reactors = formatCommaSeparatedList(senders, 6);
+            if (content) {
+                label = _t("%(reactors)s reacted with %(content)s", { reactors, content });
+            } else {
+                label = reactors;
             }
-            label = _t(
-                "<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
-                {
-                    content,
-                },
-                {
-                    reactors: () => {
-                        return formatCommaSeparatedList(senders, 6);
-                    },
-                    reactedWith: (sub) => {
-                        if (!content) {
-                            return null;
-                        }
-                        return sub;
-                    },
-                },
-            );
         }
         const isPeeking = room.getMyMembership() !== "join";
         return <AccessibleButton
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index f90643f1df..3790fc3701 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -429,7 +429,7 @@ const UserOptionsSection: React.FC<{
     if (!isMe) {
         directMessageButton = (
             <AccessibleButton onClick={() => { openDMForUser(cli, member.userId); }} className="mx_UserInfo_field">
-                { _t('Direct message') }
+                { _t("Message") }
             </AccessibleButton>
         );
     }
@@ -826,7 +826,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
     if (canAffectUser && me.powerLevel >= banPowerLevel) {
         banButton = <BanToggleButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
     }
-    if (canAffectUser && me.powerLevel >= editPowerLevel) {
+    if (canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
         muteButton = (
             <MuteToggleButton
                 member={member}
@@ -1052,8 +1052,7 @@ const PowerLevelEditor: React.FC<{
     const cli = useContext(MatrixClientContext);
 
     const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
-    const onPowerChange = useCallback(async (powerLevelStr: string) => {
-        const powerLevel = parseInt(powerLevelStr, 10);
+    const onPowerChange = useCallback(async (powerLevel: number) => {
         setSelectedPowerLevel(powerLevel);
 
         const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx
index a29bdea90b..8ed56bb2c3 100644
--- a/src/components/views/right_panel/VerificationPanel.tsx
+++ b/src/components/views/right_panel/VerificationPanel.tsx
@@ -28,7 +28,7 @@ import { SAS } from "matrix-js-sdk/src/crypto/verification/SAS";
 import VerificationQRCode from "../elements/crypto/VerificationQRCode";
 import { _t } from "../../../languageHandler";
 import SdkConfig from "../../../SdkConfig";
-import E2EIcon from "../rooms/E2EIcon";
+import E2EIcon, { E2EState } from "../rooms/E2EIcon";
 import { Phase } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
 import Spinner from "../elements/Spinner";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
@@ -189,7 +189,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
             // Element Web doesn't support scanning yet, so assume here we're the client being scanned.
             body = <React.Fragment>
                 <p>{ description }</p>
-                <E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
+                <E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
                 <div className="mx_VerificationPanel_reciprocateButtons">
                     <AccessibleButton
                         kind="danger"
@@ -252,7 +252,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
             <div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
                 <h3>{ _t("Verified") }</h3>
                 <p>{ description }</p>
-                <E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
+                <E2EIcon isUser={true} status={E2EState.Verified} size={128} hideTooltip={true} />
                 { text ? <p>{ text }</p> : null }
                 <AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
                     { _t("Got it") }
diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx
index d7493e0512..8ab73483df 100644
--- a/src/components/views/right_panel/WidgetCard.tsx
+++ b/src/components/views/right_panel/WidgetCard.tsx
@@ -97,7 +97,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
         <AppTile
             app={app}
             fullWidth
-            show
             showMenubar={false}
             room={room}
             userId={cli.getUserId()}
diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.tsx
similarity index 76%
rename from src/components/views/room_settings/RoomProfileSettings.js
rename to src/components/views/room_settings/RoomProfileSettings.tsx
index a1dfbe31dc..6533028e8c 100644
--- a/src/components/views/room_settings/RoomProfileSettings.js
+++ b/src/components/views/room_settings/RoomProfileSettings.tsx
@@ -15,27 +15,43 @@ limitations under the License.
 */
 
 import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
 import { _t } from "../../../languageHandler";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import Field from "../elements/Field";
-import * as sdk from "../../../index";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import { mediaFromMxc } from "../../../customisations/Media";
+import AccessibleButton from "../elements/AccessibleButton";
+import AvatarSetting from "../settings/AvatarSetting";
+
+interface IProps {
+    roomId: string;
+}
+
+interface IState {
+    originalDisplayName: string;
+    displayName: string;
+    originalAvatarUrl: string;
+    avatarUrl: string;
+    avatarFile: File;
+    originalTopic: string;
+    topic: string;
+    enableProfileSave: boolean;
+    canSetName: boolean;
+    canSetTopic: boolean;
+    canSetAvatar: boolean;
+}
 
 // TODO: Merge with ProfileSettings?
 @replaceableComponent("views.room_settings.RoomProfileSettings")
-export default class RoomProfileSettings extends React.Component {
-    static propTypes = {
-        roomId: PropTypes.string.isRequired,
-    };
+export default class RoomProfileSettings extends React.Component<IProps, IState> {
+    private avatarUpload = createRef<HTMLInputElement>();
 
-    constructor(props) {
+    constructor(props: IProps) {
         super(props);
 
         const client = MatrixClientPeg.get();
         const room = client.getRoom(props.roomId);
-        if (!room) throw new Error("Expected a room for ID: ", props.roomId);
+        if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`);
 
         const avatarEvent = room.currentState.getStateEvents("m.room.avatar", "");
         let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null;
@@ -60,17 +76,15 @@ export default class RoomProfileSettings extends React.Component {
             canSetTopic: room.currentState.maySendStateEvent('m.room.topic', client.getUserId()),
             canSetAvatar: room.currentState.maySendStateEvent('m.room.avatar', client.getUserId()),
         };
-
-        this._avatarUpload = createRef();
     }
 
-    _uploadAvatar = () => {
-        this._avatarUpload.current.click();
+    private uploadAvatar = (): void => {
+        this.avatarUpload.current.click();
     };
 
-    _removeAvatar = () => {
+    private removeAvatar = (): void => {
         // clear file upload field so same file can be selected
-        this._avatarUpload.current.value = "";
+        this.avatarUpload.current.value = "";
         this.setState({
             avatarUrl: null,
             avatarFile: null,
@@ -78,7 +92,7 @@ export default class RoomProfileSettings extends React.Component {
         });
     };
 
-    _cancelProfileChanges = async (e) => {
+    private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
         e.stopPropagation();
         e.preventDefault();
 
@@ -92,7 +106,7 @@ export default class RoomProfileSettings extends React.Component {
         });
     };
 
-    _saveProfile = async (e) => {
+    private saveProfile = async (e: React.FormEvent): Promise<void> => {
         e.stopPropagation();
         e.preventDefault();
 
@@ -100,35 +114,46 @@ export default class RoomProfileSettings extends React.Component {
         this.setState({ enableProfileSave: false });
 
         const client = MatrixClientPeg.get();
-        const newState = {};
+
+        let originalDisplayName: string;
+        let avatarUrl: string;
+        let originalAvatarUrl: string;
+        let originalTopic: string;
+        let avatarFile: File;
 
         // TODO: What do we do about errors?
         const displayName = this.state.displayName.trim();
         if (this.state.originalDisplayName !== this.state.displayName) {
             await client.setRoomName(this.props.roomId, displayName);
-            newState.originalDisplayName = displayName;
-            newState.displayName = displayName;
+            originalDisplayName = displayName;
         }
 
         if (this.state.avatarFile) {
             const uri = await client.uploadContent(this.state.avatarFile);
             await client.sendStateEvent(this.props.roomId, 'm.room.avatar', { url: uri }, '');
-            newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
-            newState.originalAvatarUrl = newState.avatarUrl;
-            newState.avatarFile = null;
+            avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96);
+            originalAvatarUrl = avatarUrl;
+            avatarFile = null;
         } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
             await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, '');
         }
 
         if (this.state.originalTopic !== this.state.topic) {
             await client.setRoomTopic(this.props.roomId, this.state.topic);
-            newState.originalTopic = this.state.topic;
+            originalTopic = this.state.topic;
         }
 
-        this.setState(newState);
+        this.setState({
+            originalAvatarUrl,
+            avatarUrl,
+            originalDisplayName,
+            originalTopic,
+            displayName,
+            avatarFile,
+        });
     };
 
-    _onDisplayNameChanged = (e) => {
+    private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
         this.setState({ displayName: e.target.value });
         if (this.state.originalDisplayName === e.target.value) {
             this.setState({ enableProfileSave: false });
@@ -137,7 +162,7 @@ export default class RoomProfileSettings extends React.Component {
         }
     };
 
-    _onTopicChanged = (e) => {
+    private onTopicChanged = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
         this.setState({ topic: e.target.value });
         if (this.state.originalTopic === e.target.value) {
             this.setState({ enableProfileSave: false });
@@ -146,7 +171,7 @@ export default class RoomProfileSettings extends React.Component {
         }
     };
 
-    _onAvatarChanged = (e) => {
+    private onAvatarChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
         if (!e.target.files || !e.target.files.length) {
             this.setState({
                 avatarUrl: this.state.originalAvatarUrl,
@@ -160,7 +185,7 @@ export default class RoomProfileSettings extends React.Component {
         const reader = new FileReader();
         reader.onload = (ev) => {
             this.setState({
-                avatarUrl: ev.target.result,
+                avatarUrl: String(ev.target.result),
                 avatarFile: file,
                 enableProfileSave: true,
             });
@@ -168,10 +193,7 @@ export default class RoomProfileSettings extends React.Component {
         reader.readAsDataURL(file);
     };
 
-    render() {
-        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
-        const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
-
+    public render(): JSX.Element {
         let profileSettingsButtons;
         if (
             this.state.canSetName ||
@@ -181,14 +203,14 @@ export default class RoomProfileSettings extends React.Component {
             profileSettingsButtons = (
                 <div className="mx_ProfileSettings_buttons">
                     <AccessibleButton
-                        onClick={this._cancelProfileChanges}
+                        onClick={this.cancelProfileChanges}
                         kind="link"
                         disabled={!this.state.enableProfileSave}
                     >
                         { _t("Cancel") }
                     </AccessibleButton>
                     <AccessibleButton
-                        onClick={this._saveProfile}
+                        onClick={this.saveProfile}
                         kind="primary"
                         disabled={!this.state.enableProfileSave}
                     >
@@ -200,16 +222,16 @@ export default class RoomProfileSettings extends React.Component {
 
         return (
             <form
-                onSubmit={this._saveProfile}
+                onSubmit={this.saveProfile}
                 autoComplete="off"
                 noValidate={true}
                 className="mx_ProfileSettings_profileForm"
             >
                 <input
                     type="file"
-                    ref={this._avatarUpload}
+                    ref={this.avatarUpload}
                     className="mx_ProfileSettings_avatarUpload"
-                    onChange={this._onAvatarChanged}
+                    onChange={this.onAvatarChanged}
                     accept="image/*"
                 />
                 <div className="mx_ProfileSettings_profile">
@@ -219,7 +241,7 @@ export default class RoomProfileSettings extends React.Component {
                             type="text"
                             value={this.state.displayName}
                             autoComplete="off"
-                            onChange={this._onDisplayNameChanged}
+                            onChange={this.onDisplayNameChanged}
                             disabled={!this.state.canSetName}
                         />
                         <Field
@@ -230,7 +252,7 @@ export default class RoomProfileSettings extends React.Component {
                             type="text"
                             value={this.state.topic}
                             autoComplete="off"
-                            onChange={this._onTopicChanged}
+                            onChange={this.onTopicChanged}
                             element="textarea"
                         />
                     </div>
@@ -238,8 +260,8 @@ export default class RoomProfileSettings extends React.Component {
                         avatarUrl={this.state.avatarUrl}
                         avatarName={this.state.displayName || this.props.roomId}
                         avatarAltText={_t("Room avatar")}
-                        uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
-                        removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
+                        uploadAvatar={this.state.canSetAvatar ? this.uploadAvatar : undefined}
+                        removeAvatar={this.state.canSetAvatar ? this.removeAvatar : undefined} />
                 </div>
                 { profileSettingsButtons }
             </form>
diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.tsx
similarity index 88%
rename from src/components/views/room_settings/UrlPreviewSettings.js
rename to src/components/views/room_settings/UrlPreviewSettings.tsx
index 0ff3b051d6..bb639b691a 100644
--- a/src/components/views/room_settings/UrlPreviewSettings.js
+++ b/src/components/views/room_settings/UrlPreviewSettings.tsx
@@ -18,8 +18,6 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from "../../../index";
 import { _t, _td } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import dis from "../../../dispatcher/dispatcher";
@@ -27,21 +25,22 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { Action } from "../../../dispatcher/actions";
 import { SettingLevel } from "../../../settings/SettingLevel";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { Room } from "matrix-js-sdk/src/models/room";
+import SettingsFlag from "../elements/SettingsFlag";
+
+interface IProps {
+    room: Room;
+}
 
 @replaceableComponent("views.room_settings.UrlPreviewSettings")
-export default class UrlPreviewSettings extends React.Component {
-    static propTypes = {
-        room: PropTypes.object,
-    };
-
-    _onClickUserSettings = (e) => {
+export default class UrlPreviewSettings extends React.Component<IProps> {
+    private onClickUserSettings = (e: React.MouseEvent): void => {
         e.preventDefault();
         e.stopPropagation();
         dis.fire(Action.ViewUserSettings);
     };
 
-    render() {
-        const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
+    public render(): JSX.Element {
         const roomId = this.props.room.roomId;
         const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
 
@@ -54,18 +53,18 @@ export default class UrlPreviewSettings extends React.Component {
             if (accountEnabled) {
                 previewsForAccount = (
                     _t("You have <a>enabled</a> URL previews by default.", {}, {
-                        'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
+                        'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>,
                     })
                 );
             } else {
                 previewsForAccount = (
                     _t("You have <a>disabled</a> URL previews by default.", {}, {
-                        'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
+                        'a': (sub)=><a onClick={this.onClickUserSettings} href=''>{ sub }</a>,
                     })
                 );
             }
 
-            if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
+            if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
                 previewsForRoom = (
                     <label>
                         <SettingsFlag
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.tsx
similarity index 76%
rename from src/components/views/rooms/AppsDrawer.js
rename to src/components/views/rooms/AppsDrawer.tsx
index 2866780255..5a14e6b315 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.tsx
@@ -16,7 +16,6 @@ limitations under the License.
 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import { Resizable } from "re-resizable";
 
@@ -26,8 +25,6 @@ import * as sdk from '../../../index';
 import * as ScalarMessaging from '../../../ScalarMessaging';
 import WidgetUtils from '../../../utils/WidgetUtils';
 import WidgetEchoStore from "../../../stores/WidgetEchoStore";
-import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
-import SettingsStore from "../../../settings/SettingsStore";
 import ResizeNotifier from "../../../utils/ResizeNotifier";
 import ResizeHandle from "../elements/ResizeHandle";
 import Resizer from "../../../resizer/resizer";
@@ -37,60 +34,74 @@ import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
 import { useStateCallback } from "../../../hooks/useStateCallback";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import UIStore from "../../../stores/UIStore";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { IApp } from "../../../stores/WidgetStore";
+import { ActionPayload } from "../../../dispatcher/payloads";
+
+interface IProps {
+    userId: string;
+    room: Room;
+    resizeNotifier: ResizeNotifier;
+    showApps?: boolean; // Should apps be rendered
+    maxHeight: number;
+}
+
+interface IState {
+    apps: IApp[];
+    resizingVertical: boolean; // true when changing the height of the apps drawer
+    resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
+    resizing: boolean;
+}
 
 @replaceableComponent("views.rooms.AppsDrawer")
-export default class AppsDrawer extends React.Component {
-    static propTypes = {
-        userId: PropTypes.string.isRequired,
-        room: PropTypes.object.isRequired,
-        resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
-        showApps: PropTypes.bool, // Should apps be rendered
-    };
-
-    static defaultProps = {
+export default class AppsDrawer extends React.Component<IProps, IState> {
+    private resizeContainer: HTMLDivElement;
+    private resizer: Resizer;
+    private dispatcherRef: string;
+    public static defaultProps: Partial<IProps> = {
         showApps: true,
     };
 
-    constructor(props) {
+    constructor(props: IProps) {
         super(props);
 
         this.state = {
-            apps: this._getApps(),
-            resizingVertical: false, // true when changing the height of the apps drawer
-            resizingHorizontal: false, // true when chagning the distribution of the width between widgets
+            apps: this.getApps(),
+            resizingVertical: false,
+            resizingHorizontal: false,
+            resizing: false,
         };
 
-        this._resizeContainer = null;
-        this.resizer = this._createResizer();
+        this.resizer = this.createResizer();
 
         this.props.resizeNotifier.on("isResizing", this.onIsResizing);
     }
 
-    componentDidMount() {
+    public componentDidMount(): void {
         ScalarMessaging.startListening();
-        WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
+        WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
         this.dispatcherRef = dis.register(this.onAction);
     }
 
-    componentWillUnmount() {
+    public componentWillUnmount(): void {
         ScalarMessaging.stopListening();
-        WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this._updateApps);
+        WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
         if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
-        if (this._resizeContainer) {
+        if (this.resizeContainer) {
             this.resizer.detach();
         }
         this.props.resizeNotifier.off("isResizing", this.onIsResizing);
     }
 
-    onIsResizing = (resizing) => {
+    private onIsResizing = (resizing: boolean): void => {
         // This one is the vertical, ie. change height of apps drawer
         this.setState({ resizingVertical: resizing });
         if (!resizing) {
-            this._relaxResizer();
+            this.relaxResizer();
         }
     };
 
-    _createResizer() {
+    private createResizer(): Resizer {
         // This is the horizontal one, changing the distribution of the width between the app tiles
         // (ie. a vertical resize handle because, the handle itself is vertical...)
         const classNames = {
@@ -100,11 +111,11 @@ export default class AppsDrawer extends React.Component {
         };
         const collapseConfig = {
             onResizeStart: () => {
-                this._resizeContainer.classList.add("mx_AppsDrawer_resizing");
+                this.resizeContainer.classList.add("mx_AppsDrawer_resizing");
                 this.setState({ resizingHorizontal: true });
             },
             onResizeStop: () => {
-                this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
+                this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
                 WidgetLayoutStore.instance.setResizerDistributions(
                     this.props.room, Container.Top,
                     this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
@@ -113,13 +124,13 @@ export default class AppsDrawer extends React.Component {
             },
         };
         // pass a truthy container for now, we won't call attach until we update it
-        const resizer = new Resizer({}, PercentageDistributor, collapseConfig);
+        const resizer = new Resizer(null, PercentageDistributor, collapseConfig);
         resizer.setClassNames(classNames);
         return resizer;
     }
 
-    _collectResizer = (ref) => {
-        if (this._resizeContainer) {
+    private collectResizer = (ref: HTMLDivElement): void => {
+        if (this.resizeContainer) {
             this.resizer.detach();
         }
 
@@ -127,22 +138,22 @@ export default class AppsDrawer extends React.Component {
             this.resizer.container = ref;
             this.resizer.attach();
         }
-        this._resizeContainer = ref;
-        this._loadResizerPreferences();
+        this.resizeContainer = ref;
+        this.loadResizerPreferences();
     };
 
-    _getAppsHash = (apps) => apps.map(app => app.id).join("~");
+    private getAppsHash = (apps: IApp[]): string => apps.map(app => app.id).join("~");
 
-    componentDidUpdate(prevProps, prevState) {
+    public componentDidUpdate(prevProps: IProps, prevState: IState): void {
         if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
             // Room has changed, update apps
-            this._updateApps();
-        } else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
-            this._loadResizerPreferences();
+            this.updateApps();
+        } else if (this.getAppsHash(this.state.apps) !== this.getAppsHash(prevState.apps)) {
+            this.loadResizerPreferences();
         }
     }
 
-    _relaxResizer = () => {
+    private relaxResizer = (): void => {
         const distributors = this.resizer.getDistributors();
 
         // relax all items if they had any overconstrained flexboxes
@@ -150,7 +161,7 @@ export default class AppsDrawer extends React.Component {
         distributors.forEach(d => d.finish());
     };
 
-    _loadResizerPreferences = () => {
+    private loadResizerPreferences = (): void => {
         const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
         if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
             distributions.forEach((size, i) => {
@@ -168,11 +179,11 @@ export default class AppsDrawer extends React.Component {
         }
     };
 
-    isResizing() {
+    private isResizing(): boolean {
         return this.state.resizingVertical || this.state.resizingHorizontal;
     }
 
-    onAction = (action) => {
+    private onAction = (action: ActionPayload): void => {
         const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
         switch (action.action) {
             case 'appsDrawer':
@@ -190,23 +201,15 @@ export default class AppsDrawer extends React.Component {
         }
     };
 
-    _getApps = () => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
+    private getApps = (): IApp[] => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
 
-    _updateApps = () => {
+    private updateApps = (): void => {
         this.setState({
-            apps: this._getApps(),
+            apps: this.getApps(),
         });
     };
 
-    _launchManageIntegrations() {
-        if (SettingsStore.getValue("feature_many_integration_managers")) {
-            IntegrationManagers.sharedInstance().openAll();
-        } else {
-            IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
-        }
-    }
-
-    render() {
+    public render(): JSX.Element {
         if (!this.props.showApps) return <div />;
 
         const apps = this.state.apps.map((app, index, arr) => {
@@ -257,7 +260,7 @@ export default class AppsDrawer extends React.Component {
                     className="mx_AppsContainer_resizer"
                     resizeNotifier={this.props.resizeNotifier}
                 >
-                    <div className="mx_AppsContainer" ref={this._collectResizer}>
+                    <div className="mx_AppsContainer" ref={this.collectResizer}>
                         { apps.map((app, i) => {
                             if (i < 1) return app;
                             return <React.Fragment key={app.key}>
@@ -273,7 +276,18 @@ export default class AppsDrawer extends React.Component {
     }
 }
 
-const PersistentVResizer = ({
+interface IPersistentResizerProps {
+    room: Room;
+    minHeight: number;
+    maxHeight: number;
+    className: string;
+    handleWrapperClass: string;
+    handleClass: string;
+    resizeNotifier: ResizeNotifier;
+    children: React.ReactNode;
+}
+
+const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
     room,
     minHeight,
     maxHeight,
@@ -303,7 +317,7 @@ const PersistentVResizer = ({
     });
 
     return <Resizable
-        size={{ height: Math.min(height, maxHeight) }}
+        size={{ height: Math.min(height, maxHeight), width: null }}
         minHeight={minHeight}
         maxHeight={maxHeight}
         onResizeStart={() => {
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index 48f2e2a39b..edf4f515d2 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -50,7 +50,8 @@ import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 
 // matches emoticons which follow the start of a line or whitespace
-const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
+const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
+export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
 
 const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
 
@@ -161,7 +162,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
         }
     }
 
-    private replaceEmoticon = (caretPosition: DocumentPosition): number => {
+    public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number {
         const { model } = this.props;
         const range = model.startRange(caretPosition);
         // expand range max 8 characters backwards from caretPosition,
@@ -170,9 +171,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
         range.expandBackwardsWhile((index, offset) => {
             const part = model.parts[index];
             n -= 1;
-            return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate);
+            return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type);
         });
-        const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
+        const emoticonMatch = regex.exec(range.text);
         if (emoticonMatch) {
             const query = emoticonMatch[1].replace("-", "");
             // try both exact match and lower-case, this means that xd won't match xD but :P will match :p
@@ -180,18 +181,25 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
 
             if (data) {
                 const { partCreator } = model;
-                const hasPrecedingSpace = emoticonMatch[0][0] === " ";
+                const firstMatch = emoticonMatch[0];
+                const moveStart = firstMatch[0] === " " ? 1 : 0;
+
                 // we need the range to only comprise of the emoticon
                 // because we'll replace the whole range with an emoji,
                 // so move the start forward to the start of the emoticon.
                 // Take + 1 because index is reported without the possible preceding space.
-                range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0));
+                range.moveStartForwards(emoticonMatch.index + moveStart);
+                // If the end is a trailing space/newline move end backwards, so that we don't replace it
+                if (["\n", " "].includes(firstMatch[firstMatch.length - 1])) {
+                    range.moveEndBackwards(1);
+                }
+
                 // this returns the amount of added/removed characters during the replace
                 // so the caret position can be adjusted.
-                return range.replace([partCreator.plain(data.unicode + " ")]);
+                return range.replace([partCreator.plain(data.unicode)]);
             }
         }
-    };
+    }
 
     private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
         renderModel(this.editorRef.current, this.props.model);
@@ -607,8 +615,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
     };
 
     private configureEmoticonAutoReplace = (): void => {
-        const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
-        this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null);
+        this.props.model.setTransformCallback(this.transform);
     };
 
     private configureShouldShowPillAvatar = (): void => {
@@ -621,6 +628,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
         this.setState({ surroundWith });
     };
 
+    private transform = (documentPosition: DocumentPosition): void => {
+        const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
+        if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
+    };
+
     componentWillUnmount() {
         document.removeEventListener("selectionchange", this.onSelectionChange);
         this.editorRef.current.removeEventListener("input", this.onInput, true);
diff --git a/src/components/views/rooms/E2EIcon.js b/src/components/views/rooms/E2EIcon.tsx
similarity index 60%
rename from src/components/views/rooms/E2EIcon.js
rename to src/components/views/rooms/E2EIcon.tsx
index 7425af6060..1a6db4606c 100644
--- a/src/components/views/rooms/E2EIcon.js
+++ b/src/components/views/rooms/E2EIcon.tsx
@@ -16,41 +16,51 @@ limitations under the License.
 */
 
 import React, { useState } from "react";
-import PropTypes from "prop-types";
 import classNames from 'classnames';
 
 import { _t, _td } from '../../../languageHandler';
 import AccessibleButton from "../elements/AccessibleButton";
 import Tooltip from "../elements/Tooltip";
+import { E2EStatus } from "../../../utils/ShieldUtils";
 
-export const E2E_STATE = {
-    VERIFIED: "verified",
-    WARNING: "warning",
-    UNKNOWN: "unknown",
-    NORMAL: "normal",
-    UNAUTHENTICATED: "unauthenticated",
+export enum E2EState {
+    Verified = "verified",
+    Warning = "warning",
+    Unknown = "unknown",
+    Normal = "normal",
+    Unauthenticated = "unauthenticated",
+}
+
+const crossSigningUserTitles: { [key in E2EState]?: string } = {
+    [E2EState.Warning]: _td("This user has not verified all of their sessions."),
+    [E2EState.Normal]: _td("You have not verified this user."),
+    [E2EState.Verified]: _td("You have verified this user. This user has verified all of their sessions."),
+};
+const crossSigningRoomTitles: { [key in E2EState]?: string } = {
+    [E2EState.Warning]: _td("Someone is using an unknown session"),
+    [E2EState.Normal]: _td("This room is end-to-end encrypted"),
+    [E2EState.Verified]: _td("Everyone in this room is verified"),
 };
 
-const crossSigningUserTitles = {
-    [E2E_STATE.WARNING]: _td("This user has not verified all of their sessions."),
-    [E2E_STATE.NORMAL]: _td("You have not verified this user."),
-    [E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their sessions."),
-};
-const crossSigningRoomTitles = {
-    [E2E_STATE.WARNING]: _td("Someone is using an unknown session"),
-    [E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
-    [E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
-};
+interface IProps {
+    isUser?: boolean;
+    status?: E2EState | E2EStatus;
+    className?: string;
+    size?: number;
+    onClick?: () => void;
+    hideTooltip?: boolean;
+    bordered?: boolean;
+}
 
-const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
+const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
     const [hover, setHover] = useState(false);
 
     const classes = classNames({
         mx_E2EIcon: true,
         mx_E2EIcon_bordered: bordered,
-        mx_E2EIcon_warning: status === E2E_STATE.WARNING,
-        mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
-        mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
+        mx_E2EIcon_warning: status === E2EState.Warning,
+        mx_E2EIcon_normal: status === E2EState.Normal,
+        mx_E2EIcon_verified: status === E2EState.Verified,
     }, className);
 
     let e2eTitle;
@@ -92,12 +102,4 @@ const E2EIcon = ({ isUser, status, className, size, onClick, hideTooltip, border
     </div>;
 };
 
-E2EIcon.propTypes = {
-    isUser: PropTypes.bool,
-    status: PropTypes.oneOf(Object.values(E2E_STATE)),
-    className: PropTypes.string,
-    size: PropTypes.number,
-    onClick: PropTypes.func,
-};
-
 export default E2EIcon;
diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx
index 7a3767deb7..35950492d6 100644
--- a/src/components/views/rooms/EditMessageComposer.tsx
+++ b/src/components/views/rooms/EditMessageComposer.tsx
@@ -27,7 +27,7 @@ import { findEditableEvent } from '../../../utils/EventUtils';
 import { parseEvent } from '../../../editor/deserialize';
 import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
 import EditorStateTransfer from '../../../utils/EditorStateTransfer';
-import BasicMessageComposer from "./BasicMessageComposer";
+import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
 import { Action } from "../../../dispatcher/actions";
@@ -42,6 +42,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
 import QuestionDialog from "../dialogs/QuestionDialog";
 import { ActionPayload } from "../../../dispatcher/payloads";
 import AccessibleButton from '../elements/AccessibleButton';
+import SettingsStore from "../../../settings/SettingsStore";
 
 function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
     const html = mxEvent.getContent().formatted_body;
@@ -315,6 +316,14 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
     private sendEdit = async (): Promise<void> => {
         const startTime = CountlyAnalytics.getTimestamp();
         const editedEvent = this.props.editState.getEvent();
+
+        // Replace emoticon at the end of the message
+        if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
+            const caret = this.editorRef.current?.getCaret();
+            const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
+            this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
+        }
+
         const editContent = createEditContent(this.model, editedEvent);
         const newContent = editContent["m.new_content"];
 
diff --git a/src/components/views/rooms/EntityTile.tsx b/src/components/views/rooms/EntityTile.tsx
index 88c54468d8..d7dd4b1092 100644
--- a/src/components/views/rooms/EntityTile.tsx
+++ b/src/components/views/rooms/EntityTile.tsx
@@ -20,7 +20,7 @@ import React from 'react';
 import AccessibleButton from '../elements/AccessibleButton';
 import { _td } from '../../../languageHandler';
 import classNames from "classnames";
-import E2EIcon from './E2EIcon';
+import E2EIcon, { E2EState } from './E2EIcon';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import BaseAvatar from '../avatars/BaseAvatar';
 import PresenceLabel from "./PresenceLabel";
@@ -75,7 +75,7 @@ interface IProps {
     suppressOnHover?: boolean;
     showPresence?: boolean;
     subtextLabel?: string;
-    e2eStatus?: string;
+    e2eStatus?: E2EState;
     powerStatus?: PowerStatus;
 }
 
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 315241c074..e1f0eb5368 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -21,7 +21,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
 import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { Relations } from "matrix-js-sdk/src/models/relations";
 import { RoomMember } from "matrix-js-sdk/src/models/room-member";
-import { Thread } from 'matrix-js-sdk/src/models/thread';
+import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
 
 import ReplyThread from "../elements/ReplyThread";
 import { _t } from '../../../languageHandler';
@@ -33,7 +33,7 @@ import { formatTime } from "../../../DateUtils";
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
 import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import { E2E_STATE } from "./E2EIcon";
+import { E2EState } from "./E2EIcon";
 import { toRem } from "../../../utils/units";
 import { WidgetType } from "../../../widgets/WidgetType";
 import RoomAvatar from "../avatars/RoomAvatar";
@@ -464,8 +464,8 @@ export default class EventTile extends React.Component<IProps, IState> {
         }
 
         if (SettingsStore.getValue("feature_thread")) {
-            this.props.mxEvent.once("Thread.ready", this.updateThread);
-            this.props.mxEvent.on("Thread.update", this.updateThread);
+            this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread);
+            this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
         }
     }
 
@@ -521,7 +521,7 @@ export default class EventTile extends React.Component<IProps, IState> {
 
         const thread = this.state.thread;
         const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
-        if (!thread || this.props.showThreadInfo === false) {
+        if (!thread || this.props.showThreadInfo === false || thread.length <= 1) {
             return null;
         }
 
@@ -605,7 +605,7 @@ export default class EventTile extends React.Component<IProps, IState> {
         if (encryptionInfo.mismatchedSender) {
             // something definitely wrong is going on here
             this.setState({
-                verified: E2E_STATE.WARNING,
+                verified: E2EState.Warning,
             }, this.props.onHeightChanged); // Decryption may have caused a change in size
             return;
         }
@@ -613,7 +613,7 @@ export default class EventTile extends React.Component<IProps, IState> {
         if (!userTrust.isCrossSigningVerified()) {
             // user is not verified, so default to everything is normal
             this.setState({
-                verified: E2E_STATE.NORMAL,
+                verified: E2EState.Normal,
             }, this.props.onHeightChanged); // Decryption may have caused a change in size
             return;
         }
@@ -623,27 +623,27 @@ export default class EventTile extends React.Component<IProps, IState> {
         );
         if (!eventSenderTrust) {
             this.setState({
-                verified: E2E_STATE.UNKNOWN,
+                verified: E2EState.Unknown,
             }, this.props.onHeightChanged); // Decryption may have caused a change in size
             return;
         }
 
         if (!eventSenderTrust.isVerified()) {
             this.setState({
-                verified: E2E_STATE.WARNING,
+                verified: E2EState.Warning,
             }, this.props.onHeightChanged); // Decryption may have caused a change in size
             return;
         }
 
         if (!encryptionInfo.authenticated) {
             this.setState({
-                verified: E2E_STATE.UNAUTHENTICATED,
+                verified: E2EState.Unauthenticated,
             }, this.props.onHeightChanged); // Decryption may have caused a change in size
             return;
         }
 
         this.setState({
-            verified: E2E_STATE.VERIFIED,
+            verified: E2EState.Verified,
         }, this.props.onHeightChanged); // Decryption may have caused a change in size
     }
 
@@ -850,13 +850,13 @@ export default class EventTile extends React.Component<IProps, IState> {
 
         // event is encrypted, display padlock corresponding to whether or not it is verified
         if (ev.isEncrypted()) {
-            if (this.state.verified === E2E_STATE.NORMAL) {
+            if (this.state.verified === E2EState.Normal) {
                 return; // no icon if we've not even cross-signed the user
-            } else if (this.state.verified === E2E_STATE.VERIFIED) {
+            } else if (this.state.verified === E2EState.Verified) {
                 return; // no icon for verified
-            } else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) {
+            } else if (this.state.verified === E2EState.Unauthenticated) {
                 return (<E2ePadlockUnauthenticated />);
-            } else if (this.state.verified === E2E_STATE.UNKNOWN) {
+            } else if (this.state.verified === E2EState.Unknown) {
                 return (<E2ePadlockUnknown />);
             } else {
                 return (<E2ePadlockUnverified />);
@@ -961,9 +961,9 @@ export default class EventTile extends React.Component<IProps, IState> {
             mx_EventTile_lastInSection: this.props.lastInSection,
             mx_EventTile_contextual: this.props.contextual,
             mx_EventTile_actionBarFocused: this.state.actionBarFocused,
-            mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
-            mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING,
-            mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
+            mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2EState.Verified,
+            mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2EState.Warning,
+            mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2EState.Unknown,
             mx_EventTile_bad: isEncryptionFailure,
             mx_EventTile_emote: msgtype === 'm.emote',
             mx_EventTile_noSender: this.props.hideSender,
@@ -1192,14 +1192,19 @@ export default class EventTile extends React.Component<IProps, IState> {
             }
 
             default: {
-                const thread = ReplyThread.makeThread(
-                    this.props.mxEvent,
-                    this.props.onHeightChanged,
-                    this.props.permalinkCreator,
-                    this.replyThread,
-                    this.props.layout,
-                    this.props.alwaysShowTimestamps || this.state.hover,
-                );
+                let thread;
+                // When the "showHiddenEventsInTimeline" lab is enabled,
+                // avoid showing replies for hidden events (events without tiles)
+                if (haveTileForEvent(this.props.mxEvent)) {
+                    thread = ReplyThread.makeThread(
+                        this.props.mxEvent,
+                        this.props.onHeightChanged,
+                        this.props.permalinkCreator,
+                        this.replyThread,
+                        this.props.layout,
+                        this.props.alwaysShowTimestamps || this.state.hover,
+                    );
+                }
 
                 const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
 
diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx
index dd6ce10825..332341fd23 100644
--- a/src/components/views/rooms/MessageComposer.tsx
+++ b/src/components/views/rooms/MessageComposer.tsx
@@ -32,6 +32,7 @@ import {
     ContextMenu,
     useContextMenu,
     MenuItem,
+    AboveLeftOf,
 } from "../../structures/ContextMenu";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import ReplyPreview from "./ReplyPreview";
@@ -56,7 +57,7 @@ let instanceCount = 0;
 const NARROW_MODE_BREAKPOINT = 500;
 
 interface IComposerAvatarProps {
-    me: object;
+    me: RoomMember;
 }
 
 function ComposerAvatar(props: IComposerAvatarProps) {
@@ -511,7 +512,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
                 null,
         ];
 
-        let menuPosition;
+        let menuPosition: AboveLeftOf | undefined;
         if (this.ref.current) {
             const contentRect = this.ref.current.getBoundingClientRect();
             menuPosition = aboveLeftOf(contentRect);
diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx
index 8329de7391..a97d51fc90 100644
--- a/src/components/views/rooms/NotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from "react";
+import React, { MouseEvent } from "react";
 import classNames from "classnames";
 import { formatCount } from "../../../utils/FormattingUtils";
 import SettingsStore from "../../../settings/SettingsStore";
@@ -22,6 +22,9 @@ import AccessibleButton from "../elements/AccessibleButton";
 import { XOR } from "../../../@types/common";
 import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import Tooltip from "../elements/Tooltip";
+import { _t } from "../../../languageHandler";
+import { NotificationColor } from "../../../stores/notifications/NotificationColor";
 
 interface IProps {
     notification: NotificationState;
@@ -39,6 +42,7 @@ interface IProps {
 }
 
 interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
+    showUnsentTooltip?: boolean;
     /**
      * If specified will return an AccessibleButton instead of a div.
      */
@@ -47,6 +51,7 @@ interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
 
 interface IState {
     showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
+    showTooltip: boolean;
 }
 
 @replaceableComponent("views.rooms.NotificationBadge")
@@ -59,6 +64,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
 
         this.state = {
             showCounts: SettingsStore.getValue("Notifications.alwaysShowBadgeCounts", this.roomId),
+            showTooltip: false,
         };
 
         this.countWatcherRef = SettingsStore.watchSetting(
@@ -93,9 +99,22 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
         this.forceUpdate(); // notification state changed - update
     };
 
+    private onMouseOver = (e: MouseEvent) => {
+        e.stopPropagation();
+        this.setState({
+            showTooltip: true,
+        });
+    };
+
+    private onMouseLeave = () => {
+        this.setState({
+            showTooltip: false,
+        });
+    };
+
     public render(): React.ReactElement {
         /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
-        const { notification, forceCount, roomId, onClick, ...props } = this.props;
+        const { notification, showUnsentTooltip, forceCount, roomId, onClick, ...props } = this.props;
 
         // Don't show a badge if we don't need to
         if (notification.isIdle) return null;
@@ -124,9 +143,24 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
         });
 
         if (onClick) {
+            let label: string;
+            let tooltip: JSX.Element;
+            if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) {
+                label = _t("Message didn't send. Click for info.");
+                tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
+            }
+
             return (
-                <AccessibleButton {...props} className={classes} onClick={onClick}>
+                <AccessibleButton
+                    aria-label={label}
+                    {...props}
+                    className={classes}
+                    onClick={onClick}
+                    onMouseOver={this.onMouseOver}
+                    onMouseLeave={this.onMouseLeave}
+                >
                     <span className="mx_NotificationBadge_count">{ symbol }</span>
+                    { tooltip }
                 </AccessibleButton>
             );
         }
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 541d0e1d9d..5ae2939c74 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -547,7 +547,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
                 const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
                 const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
                 // 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) {
+                if (unfilteredRooms.length < 1 && unfilteredHistorical.length < 1 && unfilteredFavourite.length < 1) {
                     explorePrompt = <div className="mx_RoomList_explorePrompt">
                         <div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
                         <AccessibleButton
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.tsx
similarity index 78%
rename from src/components/views/rooms/RoomPreviewBar.js
rename to src/components/views/rooms/RoomPreviewBar.tsx
index 89b493595f..2acefc93d6 100644
--- a/src/components/views/rooms/RoomPreviewBar.js
+++ b/src/components/views/rooms/RoomPreviewBar.tsx
@@ -14,8 +14,13 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
-import PropTypes from 'prop-types';
+import React from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixError } from "matrix-js-sdk/src/http-api";
+import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
+import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partials";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+
 import * as sdk from '../../../index';
 import { MatrixClientPeg } from '../../../MatrixClientPeg';
 import dis from '../../../dispatcher/dispatcher';
@@ -27,91 +32,102 @@ import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore
 import { UPDATE_EVENT } from "../../../stores/AsyncStore";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import InviteReason from "../elements/InviteReason";
+import { IOOBData } from "../../../stores/ThreepidInviteStore";
+import Spinner from "../elements/Spinner";
+import AccessibleButton from "../elements/AccessibleButton";
 
 const MemberEventHtmlReasonField = "io.element.html_reason";
 
-const MessageCase = Object.freeze({
-    NotLoggedIn: "NotLoggedIn",
-    Joining: "Joining",
-    Loading: "Loading",
-    Rejecting: "Rejecting",
-    Kicked: "Kicked",
-    Banned: "Banned",
-    OtherThreePIDError: "OtherThreePIDError",
-    InvitedEmailNotFoundInAccount: "InvitedEmailNotFoundInAccount",
-    InvitedEmailNoIdentityServer: "InvitedEmailNoIdentityServer",
-    InvitedEmailMismatch: "InvitedEmailMismatch",
-    Invite: "Invite",
-    ViewingRoom: "ViewingRoom",
-    RoomNotFound: "RoomNotFound",
-    OtherError: "OtherError",
-});
+enum MessageCase {
+    NotLoggedIn = "NotLoggedIn",
+    Joining = "Joining",
+    Loading = "Loading",
+    Rejecting = "Rejecting",
+    Kicked = "Kicked",
+    Banned = "Banned",
+    OtherThreePIDError = "OtherThreePIDError",
+    InvitedEmailNotFoundInAccount = "InvitedEmailNotFoundInAccount",
+    InvitedEmailNoIdentityServer = "InvitedEmailNoIdentityServer",
+    InvitedEmailMismatch = "InvitedEmailMismatch",
+    Invite = "Invite",
+    ViewingRoom = "ViewingRoom",
+    RoomNotFound = "RoomNotFound",
+    OtherError = "OtherError",
+}
+
+interface IProps {
+    // if inviterName is specified, the preview bar will shown an invite to the room.
+    // You should also specify onRejectClick if specifying inviterName
+    inviterName?: string;
+
+    // If invited by 3rd party invite, the email address the invite was sent to
+    invitedEmail?: string;
+
+    // For third party invites, information passed about the room out-of-band
+    oobData?: IOOBData;
+
+    // For third party invites, a URL for a 3pid invite signing service
+    signUrl?: string;
+
+    // A standard client/server API error object. If supplied, indicates that the
+    // caller was unable to fetch details about the room for the given reason.
+    error?: MatrixError;
+
+    canPreview?: boolean;
+    previewLoading?: boolean;
+    room?: Room;
+
+    loading?: boolean;
+    joining?: boolean;
+    rejecting?: boolean;
+    // The alias that was used to access this room, if appropriate
+    // If given, this will be how the room is referred to (eg.
+    // in error messages).
+    roomAlias?: string;
+
+    onJoinClick?(): void;
+    onRejectClick?(): void;
+    onRejectAndIgnoreClick?(): void;
+    onForgetClick?(): void;
+}
+
+interface IState {
+    busy: boolean;
+    accountEmails?: string[];
+    invitedEmailMxid?: string;
+    threePidFetchError?: MatrixError;
+}
 
 @replaceableComponent("views.rooms.RoomPreviewBar")
-export default class RoomPreviewBar extends React.Component {
-    static propTypes = {
-        onJoinClick: PropTypes.func,
-        onRejectClick: PropTypes.func,
-        onRejectAndIgnoreClick: PropTypes.func,
-        onForgetClick: PropTypes.func,
-        // if inviterName is specified, the preview bar will shown an invite to the room.
-        // You should also specify onRejectClick if specifiying inviterName
-        inviterName: PropTypes.string,
-
-        // If invited by 3rd party invite, the email address the invite was sent to
-        invitedEmail: PropTypes.string,
-
-        // For third party invites, information passed about the room out-of-band
-        oobData: PropTypes.object,
-
-        // For third party invites, a URL for a 3pid invite signing service
-        signUrl: PropTypes.string,
-
-        // A standard client/server API error object. If supplied, indicates that the
-        // caller was unable to fetch details about the room for the given reason.
-        error: PropTypes.object,
-
-        canPreview: PropTypes.bool,
-        previewLoading: PropTypes.bool,
-        room: PropTypes.object,
-
-        // When a spinner is present, a spinnerState can be specified to indicate the
-        // purpose of the spinner.
-        spinner: PropTypes.bool,
-        spinnerState: PropTypes.oneOf(["joining"]),
-        loading: PropTypes.bool,
-        joining: PropTypes.bool,
-        rejecting: PropTypes.bool,
-        // The alias that was used to access this room, if appropriate
-        // If given, this will be how the room is referred to (eg.
-        // in error messages).
-        roomAlias: PropTypes.string,
-    };
-
+export default class RoomPreviewBar extends React.Component<IProps, IState> {
     static defaultProps = {
         onJoinClick() {},
     };
 
-    state = {
-        busy: false,
-    };
+    constructor(props) {
+        super(props);
+
+        this.state = {
+            busy: false,
+        };
+    }
 
     componentDidMount() {
-        this._checkInvitedEmail();
-        CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate);
+        this.checkInvitedEmail();
+        CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
     }
 
     componentDidUpdate(prevProps, prevState) {
         if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
-            this._checkInvitedEmail();
+            this.checkInvitedEmail();
         }
     }
 
     componentWillUnmount() {
-        CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate);
+        CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
     }
 
-    async _checkInvitedEmail() {
+    private async checkInvitedEmail() {
         // If this is an invite and we've been told what email address was
         // invited, fetch the user's account emails and discovery bindings so we
         // can check them against the email that was invited.
@@ -121,8 +137,7 @@ export default class RoomPreviewBar extends React.Component {
                 // Gather the account 3PIDs
                 const account3pids = await MatrixClientPeg.get().getThreePids();
                 this.setState({
-                    accountEmails: account3pids.threepids
-                        .filter(b => b.medium === 'email').map(b => b.address),
+                    accountEmails: account3pids.threepids.filter(b => b.medium === 'email').map(b => b.address),
                 });
                 // If we have an IS connected, use that to lookup the email and
                 // check the bound MXID.
@@ -146,21 +161,21 @@ export default class RoomPreviewBar extends React.Component {
         }
     }
 
-    _onCommunityUpdate = (roomId) => {
+    private onCommunityUpdate = (roomId: string): void => {
         if (this.props.room && this.props.room.roomId !== roomId) {
             return;
         }
         this.forceUpdate(); // we have nothing to update
     };
 
-    _getMessageCase() {
+    private getMessageCase(): MessageCase {
         const isGuest = MatrixClientPeg.get().isGuest();
 
         if (isGuest) {
             return MessageCase.NotLoggedIn;
         }
 
-        const myMember = this._getMyMember();
+        const myMember = this.getMyMember();
 
         if (myMember) {
             if (myMember.isKicked()) {
@@ -195,7 +210,7 @@ export default class RoomPreviewBar extends React.Component {
             }
             return MessageCase.Invite;
         } else if (this.props.error) {
-            if (this.props.error.errcode == 'M_NOT_FOUND') {
+            if ((this.props.error as MatrixError).errcode == 'M_NOT_FOUND') {
                 return MessageCase.RoomNotFound;
             } else {
                 return MessageCase.OtherError;
@@ -205,8 +220,8 @@ export default class RoomPreviewBar extends React.Component {
         }
     }
 
-    _getKickOrBanInfo() {
-        const myMember = this._getMyMember();
+    private getKickOrBanInfo(): { memberName?: string, reason?: string } {
+        const myMember = this.getMyMember();
         if (!myMember) {
             return {};
         }
@@ -219,24 +234,19 @@ export default class RoomPreviewBar extends React.Component {
         return { memberName, reason };
     }
 
-    _joinRule() {
-        const room = this.props.room;
-        if (room) {
-            const joinRules = room.currentState.getStateEvents('m.room.join_rules', '');
-            if (joinRules) {
-                return joinRules.getContent().join_rule;
-            }
-        }
+    private joinRule(): JoinRule {
+        return this.props.room?.currentState
+            .getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
     }
 
-    _communityProfile() {
+    private communityProfile(): { displayName?: string, avatarMxc?: string } {
         if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
         return { displayName: null, avatarMxc: null };
     }
 
-    _roomName(atStart = false) {
+    private roomName(atStart = false): string {
         let name = this.props.room ? this.props.room.name : this.props.roomAlias;
-        const profile = this._communityProfile();
+        const profile = this.communityProfile();
         if (profile.displayName) name = profile.displayName;
         if (name) {
             return name;
@@ -247,14 +257,11 @@ export default class RoomPreviewBar extends React.Component {
         }
     }
 
-    _getMyMember() {
-        return (
-            this.props.room &&
-            this.props.room.getMember(MatrixClientPeg.get().getUserId())
-        );
+    private getMyMember(): RoomMember {
+        return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
     }
 
-    _getInviteMember() {
+    private getInviteMember(): RoomMember {
         const { room } = this.props;
         if (!room) {
             return;
@@ -268,8 +275,8 @@ export default class RoomPreviewBar extends React.Component {
         return room.currentState.getMember(inviterUserId);
     }
 
-    _isDMInvite() {
-        const myMember = this._getMyMember();
+    private isDMInvite(): boolean {
+        const myMember = this.getMyMember();
         if (!myMember) {
             return false;
         }
@@ -278,7 +285,7 @@ export default class RoomPreviewBar extends React.Component {
         return memberContent.membership === "invite" && memberContent.is_direct;
     }
 
-    _makeScreenAfterLogin() {
+    private makeScreenAfterLogin(): { screen: string, params: Record<string, any> } {
         return {
             screen: 'room',
             params: {
@@ -291,18 +298,16 @@ export default class RoomPreviewBar extends React.Component {
         };
     }
 
-    onLoginClick = () => {
-        dis.dispatch({ action: 'start_login', screenAfterLogin: this._makeScreenAfterLogin() });
+    private onLoginClick = () => {
+        dis.dispatch({ action: 'start_login', screenAfterLogin: this.makeScreenAfterLogin() });
     };
 
-    onRegisterClick = () => {
-        dis.dispatch({ action: 'start_registration', screenAfterLogin: this._makeScreenAfterLogin() });
+    private onRegisterClick = () => {
+        dis.dispatch({ action: 'start_registration', screenAfterLogin: this.makeScreenAfterLogin() });
     };
 
     render() {
         const brand = SdkConfig.get().brand;
-        const Spinner = sdk.getComponent('elements.Spinner');
-        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
 
         let showSpinner = false;
         let title;
@@ -315,10 +320,10 @@ export default class RoomPreviewBar extends React.Component {
         let footer;
         const extraComponents = [];
 
-        const messageCase = this._getMessageCase();
+        const messageCase = this.getMessageCase();
         switch (messageCase) {
             case MessageCase.Joining: {
-                title = _t("Joining room …");
+                title = this.props.oobData?.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
                 showSpinner = true;
                 break;
             }
@@ -349,12 +354,12 @@ export default class RoomPreviewBar extends React.Component {
                 break;
             }
             case MessageCase.Kicked: {
-                const { memberName, reason } = this._getKickOrBanInfo();
+                const { memberName, reason } = this.getKickOrBanInfo();
                 title = _t("You were kicked from %(roomName)s by %(memberName)s",
-                    { memberName, roomName: this._roomName() });
+                    { memberName, roomName: this.roomName() });
                 subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
 
-                if (this._joinRule() === "invite") {
+                if (this.joinRule() === "invite") {
                     primaryActionLabel = _t("Forget this room");
                     primaryActionHandler = this.props.onForgetClick;
                 } else {
@@ -366,9 +371,9 @@ export default class RoomPreviewBar extends React.Component {
                 break;
             }
             case MessageCase.Banned: {
-                const { memberName, reason } = this._getKickOrBanInfo();
+                const { memberName, reason } = this.getKickOrBanInfo();
                 title = _t("You were banned from %(roomName)s by %(memberName)s",
-                    { memberName, roomName: this._roomName() });
+                    { memberName, roomName: this.roomName() });
                 subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
                 primaryActionLabel = _t("Forget this room");
                 primaryActionHandler = this.props.onForgetClick;
@@ -376,8 +381,8 @@ export default class RoomPreviewBar extends React.Component {
             }
             case MessageCase.OtherThreePIDError: {
                 title = _t("Something went wrong with your invite to %(roomName)s",
-                    { roomName: this._roomName() });
-                const joinRule = this._joinRule();
+                    { roomName: this.roomName() });
+                const joinRule = this.joinRule();
                 const errCodeMessage = _t(
                     "An error (%(errcode)s) was returned while trying to validate your " +
                     "invite. You could try to pass this information on to a room admin.",
@@ -410,7 +415,7 @@ export default class RoomPreviewBar extends React.Component {
                     "This invite to %(roomName)s was sent to %(email)s which is not " +
                     "associated with your account",
                     {
-                        roomName: this._roomName(),
+                        roomName: this.roomName(),
                         email: this.props.invitedEmail,
                     },
                 );
@@ -427,7 +432,7 @@ export default class RoomPreviewBar extends React.Component {
                 title = _t(
                     "This invite to %(roomName)s was sent to %(email)s",
                     {
-                        roomName: this._roomName(),
+                        roomName: this.roomName(),
                         email: this.props.invitedEmail,
                     },
                 );
@@ -443,7 +448,7 @@ export default class RoomPreviewBar extends React.Component {
                 title = _t(
                     "This invite to %(roomName)s was sent to %(email)s",
                     {
-                        roomName: this._roomName(),
+                        roomName: this.roomName(),
                         email: this.props.invitedEmail,
                     },
                 );
@@ -458,11 +463,11 @@ export default class RoomPreviewBar extends React.Component {
             case MessageCase.Invite: {
                 const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
                 const oobData = Object.assign({}, this.props.oobData, {
-                    avatarUrl: this._communityProfile().avatarMxc,
+                    avatarUrl: this.communityProfile().avatarMxc,
                 });
                 const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
 
-                const inviteMember = this._getInviteMember();
+                const inviteMember = this.getInviteMember();
                 let inviterElement;
                 if (inviteMember) {
                     inviterElement = <span>
@@ -474,7 +479,7 @@ export default class RoomPreviewBar extends React.Component {
                     inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
                 }
 
-                const isDM = this._isDMInvite();
+                const isDM = this.isDMInvite();
                 if (isDM) {
                     title = _t("Do you want to chat with %(user)s?",
                         { user: inviteMember.name });
@@ -485,7 +490,7 @@ export default class RoomPreviewBar extends React.Component {
                     primaryActionLabel = _t("Start chatting");
                 } else {
                     title = _t("Do you want to join %(roomName)s?",
-                        { roomName: this._roomName() });
+                        { roomName: this.roomName() });
                     subTitle = [
                         avatar,
                         _t("<userName/> invited you", {}, { userName: () => inviterElement }),
@@ -519,22 +524,22 @@ export default class RoomPreviewBar extends React.Component {
             case MessageCase.ViewingRoom: {
                 if (this.props.canPreview) {
                     title = _t("You're previewing %(roomName)s. Want to join it?",
-                        { roomName: this._roomName() });
+                        { roomName: this.roomName() });
                 } else {
                     title = _t("%(roomName)s can't be previewed. Do you want to join it?",
-                        { roomName: this._roomName(true) });
+                        { roomName: this.roomName(true) });
                 }
                 primaryActionLabel = _t("Join the discussion");
                 primaryActionHandler = this.props.onJoinClick;
                 break;
             }
             case MessageCase.RoomNotFound: {
-                title = _t("%(roomName)s does not exist.", { roomName: this._roomName(true) });
+                title = _t("%(roomName)s does not exist.", { roomName: this.roomName(true) });
                 subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
                 break;
             }
             case MessageCase.OtherError: {
-                title = _t("%(roomName)s is not accessible at this time.", { roomName: this._roomName(true) });
+                title = _t("%(roomName)s is not accessible at this time.", { roomName: this.roomName(true) });
                 subTitle = [
                     _t("Try again later, or ask a room admin to check if you have access."),
                     _t(
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index cf82040793..3c9f0ea65e 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -670,6 +670,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                             onClick={this.onBadgeClick}
                             tabIndex={tabIndex}
                             aria-label={ariaLabel}
+                            showUnsentTooltip={true}
                         />
                     );
 
diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx
index 4d6de10e1f..970915d653 100644
--- a/src/components/views/rooms/RoomTile.tsx
+++ b/src/components/views/rooms/RoomTile.tsx
@@ -17,7 +17,6 @@ limitations under the License.
 
 import React, { createRef } from "react";
 import { Room } from "matrix-js-sdk/src/models/room";
-import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import classNames from "classnames";
 import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
 import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
@@ -51,8 +50,6 @@ import IconizedContextMenu, {
 } from "../context_menus/IconizedContextMenu";
 import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
-import { getUnsentMessages } from "../../structures/RoomStatusBar";
-import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
 
 interface IProps {
     room: Room;
@@ -68,7 +65,6 @@ interface IState {
     notificationsMenuPosition: PartialDOMRect;
     generalMenuPosition: PartialDOMRect;
     messagePreview?: string;
-    hasUnsentEvents: boolean;
 }
 
 const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
@@ -95,7 +91,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
             selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
             notificationsMenuPosition: null,
             generalMenuPosition: null,
-            hasUnsentEvents: this.countUnsentEvents() > 0,
 
             // generatePreview() will return nothing if the user has previews disabled
             messagePreview: "",
@@ -106,11 +101,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
         this.roomProps = EchoChamber.forRoom(this.props.room);
     }
 
-    private countUnsentEvents(): number {
-        return getUnsentMessages(this.props.room).length;
-    }
-
-    private onRoomNameUpdate = (room) => {
+    private onRoomNameUpdate = (room: Room) => {
         this.forceUpdate();
     };
 
@@ -118,11 +109,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
         this.forceUpdate(); // notification state changed - update
     };
 
-    private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
-        if (room?.roomId !== this.props.room.roomId) return;
-        this.setState({ hasUnsentEvents: this.countUnsentEvents() > 0 });
-    };
-
     private onRoomPropertyUpdate = (property: CachedRoomKey) => {
         if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
         // else ignore - not important for this tile
@@ -178,12 +164,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
         );
         this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
         this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
-        this.roomProps.on("Room.name", this.onRoomNameUpdate);
+        this.props.room?.on("Room.name", this.onRoomNameUpdate);
         CommunityPrototypeStore.instance.on(
             CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
             this.onCommunityUpdate,
         );
-        MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
     }
 
     public componentWillUnmount() {
@@ -208,7 +193,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
             CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
             this.onCommunityUpdate,
         );
-        MatrixClientPeg.get()?.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
     }
 
     private onAction = (payload: ActionPayload) => {
@@ -587,30 +571,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
         />;
 
         let badge: React.ReactNode;
-        if (!this.props.isMinimized) {
+        if (!this.props.isMinimized && this.notificationState) {
             // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
-            if (this.state.hasUnsentEvents) {
-                // hardcode the badge to a danger state when there's unsent messages
-                badge = (
-                    <div className="mx_RoomTile_badgeContainer" aria-hidden="true">
-                        <NotificationBadge
-                            notification={StaticNotificationState.RED_EXCLAMATION}
-                            forceCount={false}
-                            roomId={this.props.room.roomId}
-                        />
-                    </div>
-                );
-            } else if (this.notificationState) {
-                badge = (
-                    <div className="mx_RoomTile_badgeContainer" aria-hidden="true">
-                        <NotificationBadge
-                            notification={this.notificationState}
-                            forceCount={false}
-                            roomId={this.props.room.roomId}
-                        />
-                    </div>
-                );
-            }
+            badge = (
+                <div className="mx_RoomTile_badgeContainer" aria-hidden="true">
+                    <NotificationBadge
+                        notification={this.notificationState}
+                        forceCount={false}
+                        roomId={this.props.room.roomId}
+                    />
+                </div>
+            );
         }
 
         let messagePreview = null;
diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx
index bb5d537895..b2fca33dfe 100644
--- a/src/components/views/rooms/SendMessageComposer.tsx
+++ b/src/components/views/rooms/SendMessageComposer.tsx
@@ -31,8 +31,8 @@ import {
     textSerialize,
     unescapeMessage,
 } from '../../../editor/serialize';
+import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
 import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
-import BasicMessageComposer from "./BasicMessageComposer";
 import ReplyThread from "../elements/ReplyThread";
 import { findEditableEvent } from '../../../utils/EventUtils';
 import SendHistoryManager from "../../../SendHistoryManager";
@@ -347,15 +347,24 @@ export default class SendMessageComposer extends React.Component<IProps> {
     }
 
     public async sendMessage(): Promise<void> {
-        if (this.model.isEmpty) {
+        const model = this.model;
+
+        if (model.isEmpty) {
             return;
         }
 
+        // Replace emoticon at the end of the message
+        if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
+            const caret = this.editorRef.current?.getCaret();
+            const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
+            this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
+        }
+
         const replyToEvent = this.props.replyToEvent;
         let shouldSend = true;
         let content;
 
-        if (!containsEmote(this.model) && this.isSlashCommand()) {
+        if (!containsEmote(model) && this.isSlashCommand()) {
             const [cmd, args, commandText] = this.getSlashCommand();
             if (cmd) {
                 if (cmd.category === CommandCategories.messages) {
@@ -400,7 +409,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
             }
         }
 
-        if (isQuickReaction(this.model)) {
+        if (isQuickReaction(model)) {
             shouldSend = false;
             this.sendQuickReaction();
         }
@@ -410,7 +419,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
             const { roomId } = this.props.room;
             if (!content) {
                 content = createMessageContent(
-                    this.model,
+                    model,
                     replyToEvent,
                     this.props.replyInThread,
                     this.props.permalinkCreator,
@@ -446,9 +455,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
             CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
         }
 
-        this.sendHistoryManager.save(this.model, replyToEvent);
+        this.sendHistoryManager.save(model, replyToEvent);
         // clear composer
-        this.model.reset([]);
+        model.reset([]);
         this.editorRef.current?.clearUndoHistory();
         this.editorRef.current?.focus();
         this.clearStoredEditorState();
diff --git a/src/components/views/rooms/Stickerpicker.tsx b/src/components/views/rooms/Stickerpicker.tsx
index 0806b4ab9d..2b898e0f34 100644
--- a/src/components/views/rooms/Stickerpicker.tsx
+++ b/src/components/views/rooms/Stickerpicker.tsx
@@ -32,6 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
 import { ActionPayload } from '../../../dispatcher/payloads';
 import ScalarAuthClient from '../../../ScalarAuthClient';
 import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
+import { IApp } from "../../../stores/WidgetStore";
 
 // This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
 // We sit in a context menu, so this should be given to the context menu.
@@ -256,12 +257,16 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
             stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
 
             // FIXME: could this use the same code as other apps?
-            const stickerApp = {
+            const stickerApp: IApp = {
                 id: stickerpickerWidget.id,
                 url: stickerpickerWidget.content.url,
                 name: stickerpickerWidget.content.name,
                 type: stickerpickerWidget.content.type,
                 data: stickerpickerWidget.content.data,
+                roomId: stickerpickerWidget.content.roomId,
+                eventId: stickerpickerWidget.content.eventId,
+                avatar_url: stickerpickerWidget.content.avatar_url,
+                creatorUserId: stickerpickerWidget.content.creatorUserId,
             };
 
             stickersContent = (
@@ -287,9 +292,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
                                 onEditClick={this.launchManageIntegrations}
                                 onDeleteClick={this.removeStickerpickerWidgets}
                                 showTitle={false}
-                                showCancel={false}
                                 showPopout={false}
-                                onMinimiseClick={this.onHideStickersClick}
                                 handleMinimisePointerEvents={true}
                                 userWidget={true}
                             />
@@ -345,16 +348,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
         });
     };
 
-    /**
-     * Trigger hiding of the sticker picker overlay
-     * @param  {Event} ev Event that triggered the function call
-     */
-    private onHideStickersClick = (ev: React.MouseEvent): void => {
-        if (this.props.showStickers) {
-            this.props.setShowStickers(false);
-        }
-    };
-
     /**
      * Called when the window is resized
      */
diff --git a/src/components/views/settings/JoinRuleSettings.tsx b/src/components/views/settings/JoinRuleSettings.tsx
index 94c70f861e..4713223ec4 100644
--- a/src/components/views/settings/JoinRuleSettings.tsx
+++ b/src/components/views/settings/JoinRuleSettings.tsx
@@ -28,6 +28,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import Modal from "../../../Modal";
 import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
 import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog";
+import QuestionDialog from "../dialogs/QuestionDialog";
 import { upgradeRoom } from "../../../utils/RoomUpgrade";
 import { arrayHasDiff } from "../../../utils/arrays";
 import { useLocalEcho } from "../../../hooks/useLocalEcho";
@@ -207,27 +208,50 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
             } else if (preferredRestrictionVersion) {
                 // Block this action on a room upgrade otherwise it'd make their room unjoinable
                 const targetVersion = preferredRestrictionVersion;
-                Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
+
+                const modal = Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
                     roomId: room.roomId,
                     targetVersion,
                     description: _t("This upgrade will allow members of selected spaces " +
                         "access to this room without an invite."),
-                    onFinished: async (resp) => {
-                        if (!resp?.continue) return;
-                        const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
-                        closeSettingsFn();
-                        // switch to the new room in the background
-                        dis.dispatch({
-                            action: "view_room",
-                            room_id: roomId,
-                        });
-                        // open new settings on this tab
-                        dis.dispatch({
-                            action: "open_room_settings",
-                            initial_tab_id: ROOM_SECURITY_TAB,
-                        });
-                    },
                 });
+
+                const [resp] = await modal.finished;
+                if (!resp?.continue) return;
+
+                const userId = cli.getUserId();
+                const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId))
+                    .some(roomId => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId));
+                if (unableToUpdateSomeParents) {
+                    const modal = Modal.createTrackedDialog<[boolean]>('Parent relink warning', '', QuestionDialog, {
+                        title: _t("Before you upgrade"),
+                        description: (
+                            <div>{ _t("This room is in some spaces you’re not an admin of. " +
+                                "In those spaces, the old room will still be shown, " +
+                                "but people will be prompted to join the new one.") }</div>
+                        ),
+                        hasCancelButton: true,
+                        button: _t("Upgrade anyway"),
+                        danger: true,
+                    });
+
+                    const [shouldUpgrade] = await modal.finished;
+                    if (!shouldUpgrade) return;
+                }
+
+                const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
+                closeSettingsFn();
+                // switch to the new room in the background
+                dis.dispatch({
+                    action: "view_room",
+                    room_id: roomId,
+                });
+                // open new settings on this tab
+                dis.dispatch({
+                    action: "open_room_settings",
+                    initial_tab_id: ROOM_SECURITY_TAB,
+                });
+
                 return;
             }
 
diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
index d27910517d..f1179d38e5 100644
--- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
@@ -137,7 +137,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
         }
     }
 
-    private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => {
+    private onPowerLevelsChanged = (value: number, powerLevelKey: string) => {
         const client = MatrixClientPeg.get();
         const room = client.getRoom(this.props.roomId);
         const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
@@ -148,8 +148,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
 
         const eventsLevelPrefix = "event_levels_";
 
-        const value = parseInt(inputValue);
-
         if (powerLevelKey.startsWith(eventsLevelPrefix)) {
             // deep copy "events" object, Object.assign itself won't deep copy
             plContent["events"] = Object.assign({}, plContent["events"] || {});
@@ -181,7 +179,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
         });
     };
 
-    private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => {
+    private onUserPowerLevelChanged = (value: number, powerLevelKey: string) => {
         const client = MatrixClientPeg.get();
         const room = client.getRoom(this.props.roomId);
         const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
index 2209537967..28708fd38a 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx
@@ -28,7 +28,6 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
 import SettingsFlag from '../../../elements/SettingsFlag';
 import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
 import AccessibleButton from "../../../elements/AccessibleButton";
-import SpaceStore from "../../../../../stores/SpaceStore";
 import GroupAvatar from "../../../avatars/GroupAvatar";
 import dis from "../../../../../dispatcher/dispatcher";
 import GroupActions from "../../../../../actions/GroupActions";
@@ -145,7 +144,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
     ];
 
     static COMMUNITIES_SETTINGS = [
-        // TODO: part of delabsing move the toggle here - https://github.com/vector-im/element-web/issues/18088
+        "showCommunitiesInsteadOfSpaces",
     ];
 
     static KEYBINDINGS_SETTINGS = [
@@ -286,9 +285,17 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
         SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
     };
 
-    private renderGroup(settingIds: string[]): React.ReactNodeArray {
-        return settingIds.filter(SettingsStore.isEnabled).map(i => {
-            return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
+    private renderGroup(
+        settingIds: string[],
+        level = SettingLevel.ACCOUNT,
+        includeDisabled = false,
+    ): React.ReactNodeArray {
+        if (!includeDisabled) {
+            settingIds = settingIds.filter(SettingsStore.isEnabled);
+        }
+
+        return settingIds.map(i => {
+            return <SettingsFlag key={i} name={i} level={level} />;
         });
     }
 
@@ -334,10 +341,10 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
                     { this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
                 </div>
 
-                { SpaceStore.spacesEnabled && <div className="mx_SettingsTab_section">
+                <div className="mx_SettingsTab_section">
                     <span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
-                    { this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
-                </div> }
+                    { this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT, true) }
+                </div>
 
                 <div className="mx_SettingsTab_section">
                     <span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
@@ -349,7 +356,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
                         <p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
                         <CommunityMigrator onFinished={this.props.closeSettingsFn} />
                     </details>
-                    { this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS) }
+                    { this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS, SettingLevel.DEVICE) }
                 </div>
 
                 <div className="mx_SettingsTab_section">
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 59c970c7d0..52f7786957 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -97,9 +97,8 @@ const spaceNameValidator = withValidation({
     ],
 });
 
-const nameToAlias = (name: string, domain: string): string => {
-    const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
-    return `#${localpart}:${domain}`;
+const nameToLocalpart = (name: string): string => {
+    return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
 };
 
 // XXX: Temporary for the Spaces release only
@@ -118,9 +117,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
                         "Your feedback will help inform the next versions."),
                     rageshakeLabel: "spaces-feedback",
                     rageshakeData: Object.fromEntries([
-                        "feature_spaces.all_rooms",
-                        "feature_spaces.space_member_dms",
-                        "feature_spaces.space_dm_badges",
+                        "Spaces.allRoomsInHome",
                     ].map(k => [k, SettingsStore.getValue(k)])),
                 });
             }}
@@ -176,8 +173,9 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
             value={name}
             onChange={ev => {
                 const newName = ev.target.value;
-                if (!alias || alias === nameToAlias(name, domain)) {
-                    setAlias(nameToAlias(newName, domain));
+                if (!alias || alias === `#${nameToLocalpart(name)}:${domain}`) {
+                    setAlias(`#${nameToLocalpart(newName)}:${domain}`);
+                    aliasFieldRef.current?.validate({ allowEmpty: true });
                 }
                 setName(newName);
             }}
@@ -194,7 +192,7 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
                 onChange={setAlias}
                 domain={domain}
                 value={alias}
-                placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
+                placeholder={name ? nameToLocalpart(name) : _t("e.g. my-space")}
                 label={_t("Address")}
                 disabled={busy}
                 onKeyDown={onKeyDown}
@@ -217,6 +215,7 @@ export const SpaceCreateForm: React.FC<ISpaceCreateFormProps> = ({
 };
 
 const SpaceCreateMenu = ({ onFinished }) => {
+    const cli = useContext(MatrixClientContext);
     const [visibility, setVisibility] = useState<Visibility>(null);
     const [busy, setBusy] = useState<boolean>(false);
 
@@ -233,14 +232,18 @@ const SpaceCreateMenu = ({ onFinished }) => {
 
         setBusy(true);
         // require & validate the space name field
-        if (!await spaceNameField.current.validate({ allowEmpty: false })) {
+        if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
             spaceNameField.current.focus();
             spaceNameField.current.validate({ allowEmpty: false, focused: true });
             setBusy(false);
             return;
         }
-        // validate the space name alias field but do not require it
-        if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
+
+        // validate the space alias field but do not require it
+        const aliasLocalpart = alias.substring(1, alias.length - cli.getDomain().length - 1);
+        if (visibility === Visibility.Public && aliasLocalpart &&
+            (await spaceAliasField.current.validate({ allowEmpty: true })) === false
+        ) {
             spaceAliasField.current.focus();
             spaceAliasField.current.validate({ allowEmpty: true, focused: true });
             setBusy(false);
@@ -248,7 +251,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
         }
 
         try {
-            await createSpace(name, visibility === Visibility.Public, alias, topic, avatar);
+            await createSpace(
+                name,
+                visibility === Visibility.Public,
+                aliasLocalpart ? alias : undefined,
+                topic,
+                avatar,
+            );
 
             onFinished();
         } catch (e) {
@@ -290,13 +299,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
             />
 
             <p>
-                { _t("You can also create a Space from a <a>community</a>.", {}, {
+                { _t("You can also make Spaces from <a>communities</a>.", {}, {
                     a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
                         { sub }
                     </AccessibleButton>,
                 }) }
                 <br />
-                { _t("To join an existing space you'll need an invite.") }
+                { _t("To join a space you'll need an invite.") }
             </p>
 
             <SpaceFeedbackPrompt onClick={onFinished} />
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index d223f5b6a6..67055d7418 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -151,12 +151,19 @@ const CreateSpaceButton = ({
     }
 
     const onNewClick = menuDisplayed ? closeMenu : () => {
+        // persist that the user has interacted with this, use it to dismiss the beta dot
+        localStorage.setItem("mx_seenSpaces", "1");
         if (!isPanelCollapsed) setPanelCollapsed(true);
         openMenu();
     };
 
+    let betaDot: JSX.Element;
+    if (!localStorage.getItem("mx_seenSpaces") && !SpaceStore.instance.spacePanelSpaces.length) {
+        betaDot = <div className="mx_BetaDot" />;
+    }
+
     return <li
-        className={classNames("mx_SpaceItem", {
+        className={classNames("mx_SpaceItem mx_SpaceItem_new", {
             "collapsed": isPanelCollapsed,
         })}
         role="treeitem"
@@ -169,6 +176,7 @@ const CreateSpaceButton = ({
             onClick={onNewClick}
             isNarrow={isPanelCollapsed}
         />
+        { betaDot }
 
         { contextMenu }
     </li>;
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index 35c0275240..df6c4c8149 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -93,6 +93,7 @@ export const SpaceButton: React.FC<IButtonProps> = ({
                 notification={notificationState}
                 aria-label={ariaLabel}
                 tabIndex={tabIndex}
+                showUnsentTooltip={true}
             />
         </div>;
     }
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index cec67499ae..11206602c7 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -214,6 +214,8 @@ export default class CallView extends React.Component<IProps, IState> {
         this.setState({
             primaryFeed: primary,
             secondaryFeeds: secondary,
+            micMuted: this.props.call.isMicrophoneMuted(),
+            vidMuted: this.props.call.isLocalVideoMuted(),
         });
     };
 
@@ -258,18 +260,14 @@ export default class CallView extends React.Component<IProps, IState> {
         return { primary, secondary };
     }
 
-    private onMicMuteClick = (): void => {
+    private onMicMuteClick = async (): Promise<void> => {
         const newVal = !this.state.micMuted;
-
-        this.props.call.setMicrophoneMuted(newVal);
-        this.setState({ micMuted: newVal });
+        this.setState({ micMuted: await this.props.call.setMicrophoneMuted(newVal) });
     };
 
-    private onVidMuteClick = (): void => {
+    private onVidMuteClick = async (): Promise<void> => {
         const newVal = !this.state.vidMuted;
-
-        this.props.call.setLocalVideoMuted(newVal);
-        this.setState({ vidMuted: newVal });
+        this.setState({ vidMuted: await this.props.call.setLocalVideoMuted(newVal) });
     };
 
     private onScreenshareClick = async (): Promise<void> => {
@@ -277,9 +275,13 @@ export default class CallView extends React.Component<IProps, IState> {
         if (this.state.screensharing) {
             isScreensharing = await this.props.call.setScreensharingEnabled(false);
         } else {
-            const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
-            const [source] = await finished;
-            isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
+            if (window.electron?.getDesktopCapturerSources) {
+                const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
+                const [source] = await finished;
+                isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
+            } else {
+                isScreensharing = await this.props.call.setScreensharingEnabled(true);
+            }
         }
 
         this.setState({
diff --git a/src/editor/range.ts b/src/editor/range.ts
index 13776177a7..4336a15130 100644
--- a/src/editor/range.ts
+++ b/src/editor/range.ts
@@ -32,13 +32,20 @@ export default class Range {
         this._end = bIsLarger ? positionB : positionA;
     }
 
-    public moveStart(delta: number): void {
+    public moveStartForwards(delta: number): void {
         this._start = this._start.forwardsWhile(this.model, () => {
             delta -= 1;
             return delta >= 0;
         });
     }
 
+    public moveEndBackwards(delta: number): void {
+        this._end = this._end.backwardsWhile(this.model, () => {
+            delta -= 1;
+            return delta >= 0;
+        });
+    }
+
     public trim(): void {
         this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
         this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
diff --git a/src/hooks/useEventEmitter.ts b/src/hooks/useEventEmitter.ts
index 74b23f0198..693eebc0e3 100644
--- a/src/hooks/useEventEmitter.ts
+++ b/src/hooks/useEventEmitter.ts
@@ -20,7 +20,11 @@ import type { EventEmitter } from "events";
 type Handler = (...args: any[]) => void;
 
 // Hook to wrap event emitter on and removeListener in hook lifecycle
-export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbol, handler: Handler) => {
+export const useEventEmitter = (
+    emitter: EventEmitter | undefined,
+    eventName: string | symbol,
+    handler: Handler,
+) => {
     // Create a ref that stores handler
     const savedHandler = useRef(handler);
 
@@ -51,7 +55,11 @@ export const useEventEmitter = (emitter: EventEmitter, eventName: string | symbo
 
 type Mapper<T> = (...args: any[]) => T;
 
-export const useEventEmitterState = <T>(emitter: EventEmitter, eventName: string | symbol, fn: Mapper<T>): T => {
+export const useEventEmitterState = <T>(
+    emitter: EventEmitter | undefined,
+    eventName: string | symbol,
+    fn: Mapper<T>,
+): T => {
     const [value, setValue] = useState<T>(fn());
     const handler = useCallback((...args: any[]) => {
         setValue(fn(...args));
diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts
index e778acf8a9..89c94df10b 100644
--- a/src/hooks/useRoomState.ts
+++ b/src/hooks/useRoomState.ts
@@ -25,7 +25,7 @@ const defaultMapper: Mapper<RoomState> = (roomState: RoomState) => roomState;
 
 // Hook to simplify watching Matrix Room state
 export const useRoomState = <T extends any = RoomState>(
-    room: Room,
+    room?: Room,
     mapper: Mapper<T> = defaultMapper as Mapper<T>,
 ): T => {
     const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
diff --git a/src/i18n/strings/ar.json b/src/i18n/strings/ar.json
index aa43a5f789..d08898aff2 100644
--- a/src/i18n/strings/ar.json
+++ b/src/i18n/strings/ar.json
@@ -65,12 +65,12 @@
     "Confirm adding this email address by using Single Sign On to prove your identity.": "أكّد إضافتك لعنوان البريد هذا باستعمال الولوج الموحّد لإثبات هويّتك.",
     "Single Sign On": "الولوج الموحّد",
     "Confirm adding email": "أكّد إضافة البريد الإلكتروني",
-    "Click the button below to confirm adding this email address.": "انقر الزر أسفله لتأكيد إضافة عنوان البريد الإلكتروني هذا.",
+    "Click the button below to confirm adding this email address.": "انقر الزر بالأسفل لتأكيد إضافة عنوان البريد الإلكتروني هذا.",
     "Confirm": "أكّد",
     "Add Email Address": "أضِف بريدًا إلكترونيًا",
     "Confirm adding this phone number by using Single Sign On to prove your identity.": "أكّد إضافتك لرقم الهاتف هذا باستعمال الولوج الموحّد لإثبات هويّتك.",
     "Confirm adding phone number": "أكّد إضافة رقم الهاتف",
-    "Click the button below to confirm adding this phone number.": "انقر الزر أسفله لتأكيد إضافة رقم الهاتف هذا.",
+    "Click the button below to confirm adding this phone number.": "انقر الزر بالأسفل لتأكيد إضافة رقم الهاتف هذا.",
     "Add Phone Number": "أضِف رقم الهاتف",
     "Which officially provided instance you are using, if any": "السيرورة المقدّمة رسميًا التي تستعملها، لو وُجدت",
     "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "فيما إذا كنت تستعمل %(brand)s على جهاز اللمس فيه هو طريقة الإدخال الرئيسة",
@@ -127,7 +127,7 @@
     "PM": "م",
     "AM": "ص",
     "%(weekDayName)s %(time)s": "%(weekDayName)s ‏%(time)s",
-    "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "",
+    "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s، ‏%(day)s %(monthName)s %(fullYear)s",
     "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s، ‏%(day)s %(monthName)s %(fullYear)s ‏%(time)s",
     "Who would you like to add to this community?": "مَن تريد إضافته إلى هذا المجتمع؟",
@@ -346,9 +346,9 @@
     "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s قاعدة حظر محدثة التي طابقت %(oldGlob)s لتطابق %(newGlob)s من أجل %(reason)s",
     "Light": "ضوء",
     "Dark": "مظلم",
-    "You signed in to a new session without verifying it:": "قمت بتسجيل الدخول لجلسة جديدة من غير التحقق منها",
+    "You signed in to a new session without verifying it:": "قمت بتسجيل الدخول لجلسة جديدة من غير التحقق منها:",
     "Verify your other session using one of the options below.": "تحقق من جلستك الأخرى باستخدام أحد الخيارات في الأسفل",
-    "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s%(userId)s تم تسجيل الدخول لجلسة جديدة من غير التحقق منها",
+    "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s%(userId)s تم تسجيل الدخول لجلسة جديدة من غير التحقق منها:",
     "Ask this user to verify their session, or manually verify it below.": "اطلب من هذا المستخدم التحقق من جلسته أو تحقق منها بشكل يدوي في الأسفل",
     "Not Trusted": "غير موثوقة",
     "Manually Verify by Text": "التحقق بشكل يدوي عبر نص",
@@ -1376,7 +1376,7 @@
     "Review where you’re logged in": "راجع أماكن تسجيل دخولك",
     "No": "لا",
     "I want to help": "أريد أن أساعد",
-    "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "أرسل <UsageDataLink> بيانات استخدام مجهولة </ UseDataLink> والتي تساعدنا في تحسين %(brand)s. سيستخدم هذا <PolicyLink> ملف تعريف الارتباط </ PolicyLink>.",
+    "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "أرسل <UsageDataLink>بيانات استخدام مجهولة</UsageDataLink> والتي تساعدنا في تحسين %(brand)s. سيستخدم هذا <PolicyLink>ملف تعريف الارتباط</PolicyLink>.",
     "Help us improve %(brand)s": "ساعدنا في تحسين %(brand)s",
     "Unknown App": "تطبيق غير معروف",
     "Short keyboard patterns are easy to guess": "من السهل تخمين أنماط قصيرة من لوحة المفاتيح",
@@ -1550,17 +1550,150 @@
     "Already in call": "في مكالمة بالفعل",
     "You've reached the maximum number of simultaneous calls.": "لقد وصلت للحد الاقصى من المكالمات المتزامنة.",
     "Too Many Calls": "مكالمات كثيرة جدا",
-    "Call failed because webcam or microphone could not be accessed. Check that:": "فشلت المكالمة لعدم امكانية الوصل للميكروفون او الكاميرا , من فضلك  قم بالتأكد.",
+    "Call failed because webcam or microphone could not be accessed. Check that:": "فشلت المكالمة لعدم امكانية الوصل للميكروفون او الكاميرا، من فضلك قم بالتأكد:",
     "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح.",
     "Explore rooms": "استكشِف الغرف",
     "Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "قد يؤدي استخدام عنصر واجهة المستخدم هذا إلى مشاركة البيانات <helpIcon /> مع %(widgetDomain)s ومدير التكامل الخاص بك.",
     "Identity server is": "خادم الهوية هو",
-    "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "يتلقى مديرو التكامل بيانات الضبط ، ويمكنهم تعديل عناصر واجهة المستخدم ، وإرسال دعوات الغرف ، وتعيين مستويات القوة نيابة عنك.",
-    "Use an integration manager to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.",
-    "Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل <b>(%(serverName)s)</b> لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.",
+    "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "يتلقى مديرو التكامل بيانات الضبط، ويمكنهم تعديل عناصر واجهة المستخدم، وإرسال دعوات الغرف، وتعيين مستويات القوة نيابة عنك.",
+    "Use an integration manager to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل لإدارة البوتات وعناصر الواجهة وحزم الملصقات.",
+    "Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل <b>(%(serverName)s)</b> لإدارة البوتات وعناصر الواجهة وحزم الملصقات.",
     "Identity server": "خادوم الهوية",
     "Identity server (%(server)s)": "خادوم الهوية (%(server)s)",
     "Could not connect to identity server": "تعذر الاتصال بخادوم الهوية",
     "Not a valid identity server (status code %(code)s)": "ليس خادوم هوية صالح (رمز الحالة %(code)s)",
-    "Identity server URL must be HTTPS": "يجب أن يستعمل رابط (URL) خادوم الهوية ميفاق HTTPS"
+    "Identity server URL must be HTTPS": "يجب أن يستعمل رابط (URL) خادوم الهوية ميفاق HTTPS",
+    "%(targetName)s rejected the invitation": "رفض %(targetName)s الدعوة",
+    "%(targetName)s joined the room": "انضم %(targetName)s إلى الغرفة",
+    "%(senderName)s made no change": "لم يقم %(senderName)s بأي تغيير",
+    "%(senderName)s set a profile picture": "قام %(senderName)s بتعيين صورة رمزية",
+    "%(senderName)s changed their profile picture": "%(senderName)s قام بتغيير صورته الرمزية",
+    "Paraguay": "باراغواي",
+    "Netherlands": "هولندا",
+    "Dismiss read marker and jump to bottom": "تجاهل علامة القراءة وانتقل إلى الأسفل",
+    "Scroll up/down in the timeline": "قم بالتمرير لأعلى/لأسفل في الجدول الزمني",
+    "Toggle video on/off": "تبديل تشغيل/إيقاف الفيديو",
+    "Toggle microphone mute": "تبديل كتم صوت الميكروفون",
+    "Cancel replying to a message": "إلغاء الرد على رسالة",
+    "Jump to start/end of the composer": "انتقل إلى بداية/نهاية المؤلف",
+    "Navigate recent messages to edit": "تصفح الرسائل الأخيرة لتحريرها",
+    "New line": "سطر جديد",
+    "[number]": "[رقم]",
+    "Greece": "اليونان",
+    "%(senderName)s removed their profile picture": "%(senderName)s أزال صورة ملفه الشخصي",
+    "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s أزال اسمه (%(oldDisplayName)s)",
+    "%(senderName)s set their display name to %(displayName)s": "%(senderName)s قام بتعيين اسمه إلى %(displayName)s",
+    "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s غير اسمه إلى %(displayName)s",
+    "%(senderName)s banned %(targetName)s": "%(senderName)s حظر %(targetName)s",
+    "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s حظر %(targetName)s: %(reason)s",
+    "%(senderName)s invited %(targetName)s": "%(senderName)s دعى %(targetName)s",
+    "%(targetName)s accepted an invitation": "%(targetName)s قبل دعوة",
+    "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s قبل الدعوة ل %(displayName)s",
+    "Converts the DM to a room": "تحويل المحادثة المباشرة إلى غرفة",
+    "Converts the room to a DM": "تحويل الغرفة إلى محادثة مباشرة",
+    "Some invites couldn't be sent": "تعذر إرسال بعض الدعوات",
+    "We sent the others, but the below people couldn't be invited to <RoomName/>": "أرسلنا الآخرين، ولكن لم تتم دعوة الأشخاص أدناه إلى <RoomName/>",
+    "Zimbabwe": "زمبابوي",
+    "Yemen": "اليمن",
+    "Vietnam": "فيتنام",
+    "Venezuela": "فنزويلا",
+    "Uzbekistan": "أوزباكستان",
+    "United Arab Emirates": "الامارات العربية المتحدة",
+    "Ukraine": "اوكرانيا",
+    "Uganda": "اوغندا",
+    "Turkmenistan": "تركمانستان",
+    "Turkey": "تركيا",
+    "Tunisia": "تونس",
+    "Thailand": "تايلند",
+    "Tanzania": "تنزانيا",
+    "Tajikistan": "طاجاكستان",
+    "Taiwan": "تايوان",
+    "Syria": "سوريا",
+    "Sweden": "السويد",
+    "Sudan": "السودان",
+    "Sri Lanka": "سيريلانكا",
+    "Spain": "اسبانيا",
+    "South Sudan": "السودان الجنوبية",
+    "South Korea": "كوريا الجنوبية",
+    "South Africa": "جنوب افريقيا",
+    "Somalia": "الصومال",
+    "Slovakia": "سلوفاكيا",
+    "Singapore": "سنغافورة",
+    "Serbia": "صربيا",
+    "Senegal": "السنغال",
+    "Saudi Arabia": "المملكة العربية السعودية",
+    "Rwanda": "رواندا",
+    "Russia": "روسيا",
+    "Romania": "رومانيا",
+    "Qatar": "قطر",
+    "Portugal": "البرتغال",
+    "Poland": "بولاندا",
+    "Philippines": "الفلبين",
+    "Panama": "باناما",
+    "Palestine": "فلسطين",
+    "Pakistan": "باكستان",
+    "Oman": "عمان",
+    "Norway": "النرويج",
+    "North Korea": "كوريا الشمالية",
+    "Nigeria": "نيجيريا",
+    "Niger": "النيجر",
+    "Nicaragua": "نيكاراقوا",
+    "New Zealand": "نيوزلاندا",
+    "Nepal": "النيبال",
+    "Myanmar": "ماينمار",
+    "Mozambique": "موزمبيق",
+    "Morocco": "المغرب",
+    "Mongolia": "منغوليا",
+    "Mexico": "المكسيك",
+    "Mauritius": "موريشيوس",
+    "Mauritania": "موريتانيا",
+    "Malta": "مالطا",
+    "Mali": "مالي",
+    "Maldives": "جزر المالديف",
+    "Malaysia": "ماليزيا",
+    "Madagascar": "مدغشقر",
+    "Luxembourg": "لوكسمبرغ",
+    "Libya": "ليبيا",
+    "Liberia": "ليبريا",
+    "Lebanon": "لبنان",
+    "Latvia": "لاتفيا",
+    "Kuwait": "الكويت",
+    "Kenya": "كينيا",
+    "Kazakhstan": "كازاخستان",
+    "Jordan": "الأردن",
+    "Japan": "اليابان",
+    "Jamaica": "جامايكا",
+    "Italy": "ايطاليا",
+    "Israel": "فلسطين (اسرائيل المحتلة)",
+    "Ireland": "ايرلاندا",
+    "Iraq": "العراق",
+    "Iran": "ايران",
+    "Indonesia": "اندونيسيا",
+    "India": "الهند",
+    "Iceland": "ايسلاندا",
+    "Hungary": "هنقاريا",
+    "Hong Kong": "هونج كونج",
+    "Ghana": "غانا",
+    "Germany": "ألمانيا",
+    "Georgia": "جورجيا",
+    "France": "فرنسا",
+    "Finland": "فنلندا",
+    "Ethiopia": "اثيوبيا",
+    "Estonia": "استونيا",
+    "Eritrea": "إريتيريا",
+    "Egypt": "مصر",
+    "Ecuador": "الإكوادور",
+    "Denmark": "الدنمارك",
+    "Czech Republic": "جمهورية التشيك",
+    "Cyprus": "قبرص",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "خادمك المنزلي رفض محاولة تسجيلك الدخول. قد يكون هذا بسبب الأشياء التي تستغرق وقتًا طويلاً جدًا. الرجاء المحاولة مرة اخرى. إذا استمر هذا الأمر، يرجى الاتصال بمسؤول الخادم المنزلي.",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "تعذر الوصول إلى الخادم الرئيسي الخاص بك ولم يتمكن من تسجيل دخولك. يرجى المحاولة مرة أخرى. إذا استمر هذا ، يرجى الاتصال بمسؤول الخادم المنزلي الخاص بك.",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "لقد طلبنا من المتصفح أن يتذكر الخادم الرئيسي الذي تستخدمه للسماح لك بتسجيل الدخول، ولكن للأسف نسيه متصفحك. اذهب إلى صفحة تسجيل الدخول وحاول مرة أخرى.",
+    "Failed to transfer call": "فشل تحويل المكالمة",
+    "Transfer Failed": "فشل التحويل",
+    "Unable to transfer call": "غير قادر على تحويل المكالمة",
+    "There was an error looking up the phone number": "حدث خطأ أثناء البحث عن رقم الهاتف",
+    "Unable to look up phone number": "غير قادر على ايجاد رقم الهاتف",
+    "The user you called is busy.": "المستخدم الذي اتصلت به مشغول.",
+    "User Busy": "المستخدم مشغول"
 }
diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json
index 01d082b6a2..527176e17d 100644
--- a/src/i18n/strings/ca.json
+++ b/src/i18n/strings/ca.json
@@ -594,7 +594,7 @@
     "Files": "Fitxers",
     "You are not receiving desktop notifications": "No esteu rebent notificacions d'escriptori",
     "Friday": "Divendres",
-    "Update": "Actualització",
+    "Update": "Actualitzar",
     "What's New": "Novetats",
     "On": "Engegat",
     "Changelog": "Registre de canvis",
diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json
index 65a1fc4040..bffe3b1860 100644
--- a/src/i18n/strings/cs.json
+++ b/src/i18n/strings/cs.json
@@ -1535,7 +1535,7 @@
     "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)s neudělal(a) %(count)s krát žádnou změnu",
     "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)s neudělal(a) žádnou změnu",
     "e.g. my-room": "např. moje-mistnost",
-    "Use bots, bridges, widgets and sticker packs": "Použít roboty, propojení, widgety a balíky samolepek",
+    "Use bots, bridges, widgets and sticker packs": "Použít roboty, propojení, widgety a balíky nálepek",
     "Terms of Service": "Podmínky použití",
     "To continue you need to accept the terms of this service.": "Musíte souhlasit s podmínkami použití, abychom mohli pokračovat.",
     "Service": "Služba",
@@ -2832,7 +2832,7 @@
     "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Při vytváření vaší skupiny došlo k chybě. Název může být již obsazen nebo server nemůže zpracovat váš požadavek.",
     "See <b>%(eventType)s</b> events posted to this room": "Zobrazit události <b>%(eventType)s</b> zveřejněné v této místnosti",
     "Send stickers to your active room as you": "Poslat nálepky do vaší aktivní místnosti jako vy",
-    "Send stickers to this room as you": "Poslat samolepky jako vy do této místnosti",
+    "Send stickers to this room as you": "Poslat nálepky jako vy do této místnosti",
     "Change the topic of your active room": "Změnit téma vaší aktivní místnosti",
     "Change which room you're viewing": "Změnit kterou místnost si prohlížíte",
     "Send stickers into your active room": "Poslat nálepky do vaší aktivní místnosti",
@@ -3415,8 +3415,8 @@
     "Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Použití tohoto widgetu může sdílet data <helpIcon /> s %(widgetDomain)s a vaším správcem integrací.",
     "Identity server is": "Server identity je",
     "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Správci integrace přijímají konfigurační data a mohou vaším jménem upravovat widgety, odesílat pozvánky do místností a nastavovat úrovně oprávnění.",
-    "Use an integration manager to manage bots, widgets, and sticker packs.": "Použít správce integrací na správu botů, widgetů a samolepek.",
-    "Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Použít správce integrací <b>(%(serverName)s)</b> na správu botů, widgetů a samolepek.",
+    "Use an integration manager to manage bots, widgets, and sticker packs.": "Použít správce integrací na správu botů, widgetů a nálepek.",
+    "Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Použít správce integrací <b>(%(serverName)s)</b> na správu botů, widgetů a nálepek.",
     "Identity server": "Server identit",
     "Identity server (%(server)s)": "Server identit (%(server)s)",
     "Could not connect to identity server": "Nepodařilo se připojit k serveru identit",
@@ -3606,7 +3606,7 @@
     "<b>It's not recommended to add encryption to public rooms.</b>Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Nedoporučuje se šifrovat veřejné místnosti.</b>Veřejné místnosti může najít a připojit se k nim kdokoli, takže si v nich může číst zprávy kdokoli. Nezískáte tak žádnou z výhod šifrování a nebudete ho moci později vypnout. Šifrování zpráv ve veřejné místnosti zpomalí příjem a odesílání zpráv.",
     "Are you sure you want to add encryption to this public room?": "Opravdu chcete šifrovat tuto veřejnou místnost?",
     "Cross-signing is ready but keys are not backed up.": "Křížové podepisování je připraveno, ale klíče nejsou zálohovány.",
-    "Low bandwidth mode (requires compatible homeserver)": "Režim malé šířky pásma (vyžaduje kompatibilní homeserver)",
+    "Low bandwidth mode (requires compatible homeserver)": "Režim malé šířky pásma (vyžaduje kompatibilní domovský server)",
     "Multiple integration managers (requires manual setup)": "Více správců integrace (vyžaduje ruční nastavení)",
     "Threaded messaging": "Zprávy ve vláknech",
     "Thread": "Vlákno",
@@ -3620,5 +3620,37 @@
     "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s připnul zprávu k této místnosti. Zobrazit všechny připnuté zprávy.",
     "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s připnul <a>zprávu</a> k této místnosti. Zobrazit všechny <b>připnuté zprávy</b>.",
     "Currently, %(count)s spaces have access|one": "V současné době má prostor přístup",
-    "& %(count)s more|one": "a %(count)s další"
+    "& %(count)s more|one": "a %(count)s další",
+    "Some encryption parameters have been changed.": "Byly změněny některé parametry šifrování.",
+    "Role in <RoomName/>": "Role v <RoomName/>",
+    "Send a sticker": "Odeslat nálepku",
+    "Explore %(spaceName)s": "Prozkoumat %(spaceName)s",
+    "Reply to encrypted thread…": "Odpovědět na zašifrované vlákno…",
+    "Reply to thread…": "Odpovědět na vlákno…",
+    "Add emoji": "Přidat emoji",
+    "Unknown failure": "Neznámá chyba",
+    "Failed to update the join rules": "Nepodařilo se aktualizovat pravidla pro připojení",
+    "Select the roles required to change various parts of the space": "Výbrat role potřebné ke změně různých částí prostoru",
+    "Change description": "Změnit popis",
+    "Change main address for the space": "Změnit hlavní adresu prostoru",
+    "Change space name": "Změnit název prostoru",
+    "Change space avatar": "Změnit avatar prostoru",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Kdokoli v <spaceName/> může prostor najít a připojit se. Můžete vybrat i další prostory.",
+    "Message didn't send. Click for info.": "Zpráva se neodeslala. Klikněte pro informace.",
+    "To join this Space, hide communities in your <a>preferences</a>": "Pro připojení k tomuto prostoru, skryjte zobrazení skupin v <a>předvolbách</a>",
+    "To view this Space, hide communities in your <a>preferences</a>": "Pro zobrazení tohoto prostoru, skryjte zobrazení skupin v <a>předvolbách</a>",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "Pro připojení k %(communityName)s, přepněte na skupiny v <a>předvolbách</a>",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "Pro zobrazní %(communityName)s, přepněte na zobrazení skupin v <a>předvolbách</a>",
+    "Private community": "Soukromá skupina",
+    "Public community": "Veřejná skupina",
+    "Message": "Zpráva",
+    "Upgrade anyway": "I přesto aktualizovat",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Tato místnost se nachází v některých prostorech, jejichž nejste správcem. V těchto prostorech bude stará místnost stále zobrazena, ale lidé budou vyzváni, aby se připojili k nové místnosti.",
+    "Before you upgrade": "Než provedete aktualizaci",
+    "To join a space you'll need an invite.": "Pro připojení k prostoru potřebujete pozvánku.",
+    "You can also make Spaces from <a>communities</a>.": "Můžete také vytvořit prostory ze <a>skupin</a>.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Dočasně zobrazit skupiny místo prostorů pro tuto relaci. Podpora bude v blízké budoucnosti odstraněna. Toto provede přenačtení Elementu.",
+    "Display Communities instead of Spaces": "Zobrazit skupiny místo prostorů",
+    "Joining space …": "Připojování k prostoru…",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s reagoval(a) na %(content)s"
 }
diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json
index 9ee1f56d55..432f4251ec 100644
--- a/src/i18n/strings/de_DE.json
+++ b/src/i18n/strings/de_DE.json
@@ -892,7 +892,7 @@
     "<b>Save it</b> on a USB key or backup drive": "<b>Speichere ihn</b> auf einem USB-Schlüssel oder Sicherungslaufwerk",
     "<b>Copy it</b> to your personal cloud storage": "<b>Kopiere ihn</b> in deinen persönlichen Cloud-Speicher",
     "Unable to create key backup": "Konnte Schlüsselsicherung nicht erstellen",
-    "Retry": "Erneut probieren",
+    "Retry": "Wiederholen",
     "Unable to restore backup": "Konnte Schlüsselsicherung nicht wiederherstellen",
     "No backup found!": "Keine Schlüsselsicherung gefunden!",
     "This looks like a valid recovery key!": "Dies sieht wie ein gültiger Wiederherstellungsschlüssel aus!",
@@ -3642,5 +3642,41 @@
     "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.": "Dieser Raum ist nicht verschlüsselt. Oft ist dies aufgrund eines nicht unterstützten Geräts oder Methode wie E-Mail-Einladungen der Fall.",
     "Cross-signing is ready but keys are not backed up.": "Quersignatur ist bereit, die Schlüssel sind aber nicht gesichert.",
     "Help people in spaces to find and join private rooms": "Hilf Personen in Spaces, privaten Räumen beizutreten",
-    "Use Command + F to search timeline": "Nutze Command + F um den Verlauf zu durchsuchen"
+    "Use Command + F to search timeline": "Nutze Command + F um den Verlauf zu durchsuchen",
+    "Reply to thread…": "Nachricht an Thread senden…",
+    "Reply to encrypted thread…": "Verschlüsselte Nachricht an Thread senden…",
+    "Role in <RoomName/>": "Rolle in <RoomName/>",
+    "Results": "Ergebnisse",
+    "Rooms and spaces": "Räume und Spaces",
+    "Thread": "Thread",
+    "Show threads": "Threads anzeigen",
+    "Explore %(spaceName)s": "%(spaceName)s erkunden",
+    "Send a sticker": "Sticker senden",
+    "Add emoji": "Emoji hinzufügen",
+    "Are you sure you want to make this encrypted room public?": "Willst du diesen verschlüsselten Raum wirklich öffentlich machen?",
+    "Unknown failure": "Unbekannter Fehler",
+    "Failed to update the join rules": "Fehler beim updaten der Beitrittsregeln",
+    "To avoid these issues, create a <a>new encrypted room</a> for the conversation you plan to have.": "Um dieses Problem zu vermeiden, <a>erstelle einen neuen verschlüsselten Raum</a> für deine Konversation.",
+    "<b>It's not recommended to add encryption to public rooms.</b>Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Verschlüsselung ist für öffentliche Räume nicht empfohlen.</b> Jeder kann den Raum betreten und so auch Nachrichten lesen und Senden und Empfangen wird langsamer. Du hast daher von der Verschlüsselung keinen Vorteil und kannst sie später nicht mehr ausschalten.",
+    "Are you sure you want to add encryption to this public room?": "Dieser Raum ist öffentlich. Willst du die Verschlüsselung wirklich aktivieren?",
+    "Change description": "Beschreibung bearbeiten",
+    "Change main address for the space": "Hauptadresse des Space ändern",
+    "Change space name": "Name des Space ändern",
+    "Change space avatar": "Space-Icon ändern",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Alle in <spaceName/> kann beitreten. Du kannst auch weitere Spaces auswählen.",
+    "Currently, %(count)s spaces have access|one": "Derzeit hat ein Space Zugriff",
+    "& %(count)s more|one": "und %(count)s weitere",
+    "Low bandwidth mode (requires compatible homeserver)": "Modus für niedrige Bandweite (kompatibler Heimserver benötigt)",
+    "Autoplay videos": "Videos automatisch abspielen",
+    "Autoplay GIFs": "GIFs automatisch abspielen",
+    "Multiple integration managers (requires manual setup)": "Mehrere Integrationsmanager (benötigt manuelles Einrichten)",
+    "Threaded messaging": "Threads",
+    "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s hat eine Nachricht losgelöst. Alle angepinnten Nachrichten anzeigen.",
+    "%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s hat <a>eine Nachricht</a> losgeheftet. Alle <b>angehefteten Nachrichten anzeigen</b>.",
+    "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s hat eine Nachricht angeheftet. Alle angehefteten Nachrichten anzeigen.",
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s hat <a>eine Nachricht</a> angeheftet. <b>Alle angehefteten Nachrichten anzeigen</b>.",
+    "Joining space …": "Space beitreten…",
+    "To join a space you'll need an invite.": "Um einem Space beizutreten brauchst du eine Einladung.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "In dieser Sitzung temporär Communities statt Spaces anzeigen. Unterstützung hierfür wird in naher Zukunft entfernt. Dies wird Element neu laden.",
+    "Display Communities instead of Spaces": "Communities statt Spaces anzeigen"
 }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b9923f4bc1..d60f14f4f2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -799,15 +799,6 @@
     "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
     "Change notification settings": "Change notification settings",
     "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
-    "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
-    "Spaces": "Spaces",
-    "Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
-    "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.",
-    "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta available for web, desktop and Android. Thank you for trying the beta.",
-    "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.",
-    "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
-    "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
-    "Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
     "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
     "Render LaTeX maths in messages": "Render LaTeX maths in messages",
     "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
@@ -883,6 +874,8 @@
     "Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)",
     "Show all rooms in Home": "Show all rooms in Home",
     "All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.",
+    "Display Communities instead of Spaces": "Display Communities instead of Spaces",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.",
     "Collecting app version information": "Collecting app version information",
     "Collecting logs": "Collecting logs",
     "Uploading logs": "Uploading logs",
@@ -1033,14 +1026,15 @@
     "e.g. my-space": "e.g. my-space",
     "Address": "Address",
     "Create a space": "Create a space",
+    "Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
     "What kind of Space do you want to create?": "What kind of Space do you want to create?",
     "You can change this later.": "You can change this later.",
     "Public": "Public",
     "Open space for anyone, best for communities": "Open space for anyone, best for communities",
     "Private": "Private",
     "Invite only, best for yourself or teams": "Invite only, best for yourself or teams",
-    "You can also create a Space from a <a>community</a>.": "You can also create a Space from a <a>community</a>.",
-    "To join an existing space you'll need an invite.": "To join an existing space you'll need an invite.",
+    "You can also make Spaces from <a>communities</a>.": "You can also make Spaces from <a>communities</a>.",
+    "To join a space you'll need an invite.": "To join a space you'll need an invite.",
     "Go back": "Go back",
     "Your public space": "Your public space",
     "Your private space": "Your private space",
@@ -1052,6 +1046,7 @@
     "Show all rooms": "Show all rooms",
     "All rooms": "All rooms",
     "Options": "Options",
+    "Spaces": "Spaces",
     "Expand space panel": "Expand space panel",
     "Collapse space panel": "Collapse space panel",
     "Click to copy": "Click to copy",
@@ -1162,6 +1157,9 @@
     "Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.",
     "Space members": "Space members",
     "This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.",
+    "Before you upgrade": "Before you upgrade",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.",
+    "Upgrade anyway": "Upgrade anyway",
     "Message layout": "Message layout",
     "IRC": "IRC",
     "Modern": "Modern",
@@ -1598,6 +1596,7 @@
     "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.": "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.",
     "Enable encryption in settings.": "Enable encryption in settings.",
     "End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled",
+    "Message didn't send. Click for info.": "Message didn't send. Click for info.",
     "Unpin": "Unpin",
     "View message": "View message",
     "%(duration)ss": "%(duration)ss",
@@ -1661,6 +1660,7 @@
     "%(count)s results|other": "%(count)s results",
     "%(count)s results|one": "%(count)s result",
     "This room": "This room",
+    "Joining space …": "Joining space …",
     "Joining room …": "Joining room …",
     "Loading …": "Loading …",
     "Rejecting invite …": "Rejecting invite …",
@@ -1838,7 +1838,7 @@
     "Mention": "Mention",
     "Invite": "Invite",
     "Share Link to User": "Share Link to User",
-    "Direct message": "Direct message",
+    "Message": "Message",
     "Demote yourself?": "Demote yourself?",
     "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.",
     "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
@@ -1977,7 +1977,7 @@
     "Add reaction": "Add reaction",
     "Show all": "Show all",
     "Reactions": "Reactions",
-    "<reactors/><reactedWith> reacted with %(content)s</reactedWith>": "<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s",
     "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>": "<reactors/><reactedWith>reacted with %(shortName)s</reactedWith>",
     "Message deleted": "Message deleted",
     "Message deleted by %(name)s": "Message deleted by %(name)s",
@@ -2430,15 +2430,16 @@
     "Clear cache and resync": "Clear cache and resync",
     "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
     "Updating %(brand)s": "Updating %(brand)s",
-    "Don't leave any": "Don't leave any",
-    "Leave all rooms and spaces": "Leave all rooms and spaces",
-    "Leave specific rooms and spaces": "Leave specific rooms and spaces",
+    "Don't leave any rooms": "Don't leave any rooms",
+    "Leave all rooms": "Leave all rooms",
+    "Leave some rooms": "Leave some rooms",
     "Search %(spaceName)s": "Search %(spaceName)s",
     "You won't be able to rejoin unless you are re-invited.": "You won't be able to rejoin unless you are re-invited.",
     "You're the only admin of this space. Leaving it will mean no one has control over it.": "You're the only admin of this space. Leaving it will mean no one has control over it.",
     "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.": "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.",
     "Leave %(spaceName)s": "Leave %(spaceName)s",
-    "Are you sure you want to leave <spaceName/>?": "Are you sure you want to leave <spaceName/>?",
+    "You are about to leave <spaceName/>.": "You are about to leave <spaceName/>.",
+    "Would you like to leave the rooms in this space?": "Would you like to leave the rooms in this space?",
     "Leave space": "Leave space",
     "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.",
     "Start using Key Backup": "Start using Key Backup",
@@ -2787,6 +2788,10 @@
     "Create a Group Chat": "Create a Group Chat",
     "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s",
     "Open dial pad": "Open dial pad",
+    "Public community": "Public community",
+    "Private community": "Private community",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "To view %(communityName)s, swap to communities in your <a>preferences</a>",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "To join %(communityName)s, swap to communities in your <a>preferences</a>",
     "Failed to reject invitation": "Failed to reject invitation",
     "Cannot create rooms in this community": "Cannot create rooms in this community",
     "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",
@@ -2817,7 +2822,6 @@
     "Error whilst fetching joined communities": "Error whilst fetching joined communities",
     "Create a new community": "Create a new community",
     "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
-    "Communities are changing to Spaces": "Communities are changing to Spaces",
     "You’re all caught up": "You’re all caught up",
     "You have no visible notifications.": "You have no visible notifications.",
     "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
@@ -2884,8 +2888,8 @@
     "Search names and descriptions": "Search names and descriptions",
     "Private space": "Private space",
     "<inviter/> invites you": "<inviter/> invites you",
-    "To view %(spaceName)s, turn on the <a>Spaces beta</a>": "To view %(spaceName)s, turn on the <a>Spaces beta</a>",
-    "To join %(spaceName)s, turn on the <a>Spaces beta</a>": "To join %(spaceName)s, turn on the <a>Spaces beta</a>",
+    "To view this Space, hide communities in your <a>preferences</a>": "To view this Space, hide communities in your <a>preferences</a>",
+    "To join this Space, hide communities in your <a>preferences</a>": "To join this Space, hide communities in your <a>preferences</a>",
     "To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite",
     "Created from <Community />": "Created from <Community />",
     "Welcome to <name/>": "Welcome to <name/>",
diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json
index 823384e306..dc698235e0 100644
--- a/src/i18n/strings/eo.json
+++ b/src/i18n/strings/eo.json
@@ -1318,7 +1318,7 @@
     "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Vakigo de la memoro de via foliumilo eble korektos la problemon, sed adiaŭigos vin, kaj malebligos legadon de historio de ĉifritaj babiloj.",
     "Missing session data": "Mankas datumoj de salutaĵo",
     "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Iuj datumoj de salutaĵo, inkluzive viajn ĉifrajn ŝlosilojn, mankas. Por tion korekti, resalutu, kaj rehavu la ŝlosilojn el savkopio.",
-    "Your browser likely removed this data when running low on disk space.": "Via foliumilo probable forigos ĉi tiujn datumojn kiam al ĝi mankos spaco sur disko.",
+    "Your browser likely removed this data when running low on disk space.": "Via foliumilo verŝajne forigos ĉi tiujn datumojn kiam al ĝi mankos spaco sur disko.",
     "Unable to restore backup": "Ne povas rehavi savkopion",
     "Failed to decrypt %(failedCount)s sessions!": "Malsukcesis malĉifri%(failedCount)s salutaĵojn!",
     "<b>Warning</b>: you should only set up key backup from a trusted computer.": "<b>Averto</b>: vi agordu ŝlosilan savkopion nur per fidata komputilo.",
@@ -2395,7 +2395,7 @@
     "Show message previews for reactions in all rooms": "Montri antaŭrigardojn al mesaĝoj ĉe reagoj en ĉiuj ĉambroj",
     "Your server isn't responding to some <a>requests</a>.": "Via servilo ne respondas al iuj <a>petoj</a>.",
     "Server isn't responding": "Servilo ne respondas",
-    "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Via servilo ne respondas al iuj el viaj petoj. Vidu sube kelkon de la plej probablaj kialoj.",
+    "Your server isn't responding to some of your requests. Below are some of the most likely reasons.": "Via servilo ne respondas al iuj el viaj petoj. Vidu sube kelkon de la plej verŝajnaj kialoj.",
     "The server (%(serverName)s) took too long to respond.": "La servilo (%(serverName)s) tro longe ne respondis.",
     "Your firewall or anti-virus is blocking the request.": "Via fajroŝirmilo aŭ kontraŭvirusilo blokas la peton.",
     "A browser extension is preventing the request.": "Kromprogramo de la foliumilo malhelpas la peton.",
@@ -3319,7 +3319,7 @@
     "Verify other login": "Kontroli alian saluton",
     "Reset event store": "Restarigi deponejon de okazoj",
     "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Se vi tamen tion faras, sciu ke neniu el viaj mesaĝoj foriĝos, sed via sperto pri serĉado povas malboniĝi momente, dum la indekso estas refarata",
-    "You most likely do not want to reset your event index store": "Plej probable, vi ne volas restarigi vian deponejon de indeksoj de okazoj",
+    "You most likely do not want to reset your event index store": "Plej verŝajne, vi ne volas restarigi vian deponejon de indeksoj de okazoj",
     "Reset event store?": "Ĉu restarigi deponejon de okazoj?",
     "Currently joining %(count)s rooms|one": "Nun aliĝante al %(count)s ĉambro",
     "Currently joining %(count)s rooms|other": "Nun aliĝante al %(count)s ĉambroj",
@@ -3604,5 +3604,6 @@
     "You can change this later.": "Vi povas ŝanĝi ĉi tion poste.",
     "What kind of Space do you want to create?": "Kian aron volas vi krei?",
     "All rooms you're in will appear in Home.": "Ĉiuj ĉambroj, kie vi estas, aperos en la ĉefpaĝo.",
-    "Show all rooms in Home": "Montri ĉiujn ĉambrojn en ĉefpaĝo"
+    "Show all rooms in Home": "Montri ĉiujn ĉambrojn en ĉefpaĝo",
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fiksis <a>mesaĝon</a> al ĉi tiu ĉambro. Vidu ĉiujn <b>fiksitajn mesaĝojn</b>."
 }
diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json
index 69110f4aea..7504c0d09a 100644
--- a/src/i18n/strings/es.json
+++ b/src/i18n/strings/es.json
@@ -3638,5 +3638,35 @@
     "The above, but in any room you are joined or invited to as well": "Lo de arriba, pero en cualquier sala en la que estés o te inviten",
     "The above, but in <Room /> as well": "Lo de arriba, pero también en <Room />",
     "Autoplay videos": "Reproducir automáticamente los vídeos",
-    "Autoplay GIFs": "Reproducir automáticamente los GIFs"
+    "Autoplay GIFs": "Reproducir automáticamente los GIFs",
+    "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s ha anclado un mensaje en esta sala. Mira todos los mensajes anclados.",
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s ha anclado <a>un mensaje</a> en esta sala. Mira todos los <b>mensajes anclados</b>.",
+    "Some encryption parameters have been changed.": "Algunos parámetros del cifrado han cambiado.",
+    "Role in <RoomName/>": "Rol en <RoomName/>",
+    "Currently, %(count)s spaces have access|one": "Ahora mismo, un espacio tiene acceso",
+    "& %(count)s more|one": "y %(count)s más",
+    "Select the roles required to change various parts of the space": "Selecciona los roles necesarios para cambiar varios ajustes del espacio",
+    "Failed to update the join rules": "Fallo al actualizar las reglas para unirse",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Cualquiera en <spaceName/> puede encontrar y unirse. También puedes seleccionar otros espacios.",
+    "Explore %(spaceName)s": "Explorar %(spaceName)s",
+    "Send a sticker": "Enviar una pegatina",
+    "Reply to encrypted thread…": "Responder al tema cifrado…",
+    "Reply to thread…": "Responder al tema…",
+    "Add emoji": "Añadir emojis",
+    "Unknown failure": "Fallo desconocido",
+    "Change space avatar": "Cambiar la imagen del espacio",
+    "Change space name": "Cambiar el nombre del espacio",
+    "Change main address for the space": "Cambiar dirección principal del espacio",
+    "Change description": "Cambiar descripción",
+    "Private community": "Comunidad privada",
+    "Public community": "Comunidad pública",
+    "Message": "Mensaje",
+    "Joining space …": "Uniéndote al espacio…",
+    "Message didn't send. Click for info.": "Mensaje no enviado. Haz clic para más info.",
+    "Upgrade anyway": "Actualizar de todos modos",
+    "Before you upgrade": "Antes de actualizar",
+    "To join a space you'll need an invite.": "Para unirte a un espacio, necesitas que te inviten a él.",
+    "You can also make Spaces from <a>communities</a>.": "También puedes crear espacios a partir de <a>comunidades</a>.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Ver temporalmente comunidades en vez de espacios durante esta sesión. Esta opción desaparecerá en el futuro. Element se recargará.",
+    "Display Communities instead of Spaces": "Ver comunidades en vez de espacios"
 }
diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json
index 817ff8d312..d2ebbc84ab 100644
--- a/src/i18n/strings/et.json
+++ b/src/i18n/strings/et.json
@@ -3674,5 +3674,39 @@
     "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s eemaldas siin jututoas klammerduse ühelt sõnumilt. Vaata kõiki klammerdatud sõnumeid.",
     "%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s eemaldas siin jututoas klammerduse <a>ühelt sõnumilt</a>. Vaata kõiki <b>klammerdatud sõnumeid</b>.",
     "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s klammerdas siin jututoas <a>ühe sõnumi</a>. Vaata kõiki <b>klammerdatud sõnumeid</b>.",
-    "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s klammerdas siin jututoas ühe sõnumi. Vaata kõiki klammerdatud sõnumeid."
+    "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s klammerdas siin jututoas ühe sõnumi. Vaata kõiki klammerdatud sõnumeid.",
+    "Some encryption parameters have been changed.": "Mõned krüptimise parameetrid on muutunud.",
+    "Role in <RoomName/>": "Roll jututoas <RoomName/>",
+    "Add emoji": "Lisa emoji",
+    "Reply to encrypted thread…": "Vasta krüptitud jutulõngas…",
+    "Reply to thread…": "Vasta jutulõngas…",
+    "Send a sticker": "Saada kleeps",
+    "Explore %(spaceName)s": "Tutvu kogukonnaga - %(spaceName)s",
+    "Unknown failure": "Määratlemata viga",
+    "Failed to update the join rules": "Liitumisreeglite uuendamine ei õnnestunud",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Kõik <spaceName/> kogukonnakeskuse liikmed saavad leida ja liituda. Sa võid valida ka muid kogukonnakeskuseid.",
+    "Select the roles required to change various parts of the space": "Vali rollid, mis on vajalikud kogukonna eri osade muutmiseks",
+    "Change description": "Muuda kirjeldust",
+    "Change main address for the space": "Muuda kogukonna põhiaadressi",
+    "Change space name": "Muuda kogukonna nime",
+    "Change space avatar": "Muuda kogukonna tunnuspilti",
+    "Displaying time": "Aegade kuvamine",
+    "Message didn't send. Click for info.": "Sõnum jäi saatmata. Lisateabe saamiseks klõpsi.",
+    "To join this Space, hide communities in your <a>preferences</a>": "Selle uut tüüpi kogukonnakeskusega liitumiseks peida <a>seadistustest</a> vanad kogukonnad",
+    "To view this Space, hide communities in your <a>preferences</a>": "Selle uut tüüpi kogukonnakeskuse nägemiseks peida <a>seadistustest</a> vanad kogukonnad",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "Liitumaks %(communityName)s kogukonnaga võta <a>seadistustes</a> kasutusele vana tüüpi kogukonnad",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "Senise %(communityName)s kogukonna nägemiseks võta <a>seadistustes</a> kasutusele vana tüüpi kogukonnad",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "See jututuba on mõne sellise kogukonnakeskuse osa, kus sul pole haldaja õigusi. Selliselt juhul vana jututuba jätkuvalt kuvatakse, kuid selle asutajatele pakutakse võimalust uuega liituda.",
+    "You can also make Spaces from <a>communities</a>.": "Sa võid ka <a>vana kogukonna</a> muuta uueks kogukonnakeskuseks.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Ajutiselt näita selles sessioonis uute kogukonnakeskuste asemel vanu kogukondi. Lähitulevikus selline funktsionaalsus kaob. Eeldab rakenduse taaskäivitamist.",
+    "Display Communities instead of Spaces": "Kuva uute kogukonnakeskuste asemel vanu kogukondi",
+    "[number]": "[number]",
+    "Private community": "Privaatne kogukond",
+    "Public community": "Avalik kogukond",
+    "Message": "Sõnum",
+    "Upgrade anyway": "Uuenda ikkagi",
+    "Before you upgrade": "Enne uuendamist",
+    "To join a space you'll need an invite.": "Kogukonnakeskusega liitumiseks vajad kutset.",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s kasutajat reageeris järgnevalt: %(content)s",
+    "Joining space …": "Liitun kohukonnakeskusega…"
 }
diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json
index 862cefe06d..1a20e7ba5d 100644
--- a/src/i18n/strings/fr.json
+++ b/src/i18n/strings/fr.json
@@ -807,7 +807,7 @@
     "e.g. %(exampleValue)s": "par ex. %(exampleValue)s",
     "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "La visibilité des messages dans Matrix est la même que celle des e-mails. Quand nous oublions vos messages, cela signifie que les messages que vous avez envoyés ne seront partagés avec aucun nouvel utilisateur ou avec les utilisateurs non enregistrés, mais les utilisateurs enregistrés qui ont déjà eu accès à ces messages en conserveront leur propre copie.",
     "Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Veuillez oublier tous les messages que j’ai envoyé quand mon compte sera désactivé (<b>Avertissement :</b> les futurs utilisateurs verront des conversations incomplètes)",
-    "Can't leave Server Notices room": "Impossible de quitter le salon des annonces au serveur",
+    "Can't leave Server Notices room": "Impossible de quitter le salon des Annonces du Serveur",
     "This room is used for important messages from the Homeserver, so you cannot leave it.": "Ce salon est utilisé pour les messages importants du serveur d’accueil, vous ne pouvez donc pas en partir.",
     "No Audio Outputs detected": "Aucune sortie audio détectée",
     "Audio Output": "Sortie audio",
@@ -977,7 +977,7 @@
     "%(names)s and %(count)s others are typing …|one": "%(names)s et un autre sont en train d’écrire…",
     "%(names)s and %(lastPerson)s are typing …": "%(names)s et %(lastPerson)s sont en train d’écrire…",
     "Enable Emoji suggestions while typing": "Activer la suggestion d’émojis lors de la saisie",
-    "Render simple counters in room header": "Afficher des compteurs simples dans l’en-tête des salons",
+    "Render simple counters in room header": "Afficher des compteurs simplifiés dans l’en-tête des salons",
     "Show a placeholder for removed messages": "Afficher les messages supprimés",
     "Show join/leave messages (invites/kicks/bans unaffected)": "Afficher les messages d’arrivée et de départ (les invitations/expulsions/bannissements ne sont pas concernés)",
     "Show avatar changes": "Afficher les changements d’avatar",
@@ -1523,8 +1523,8 @@
     "Create a private room": "Créer un salon privé",
     "Topic (optional)": "Sujet (facultatif)",
     "Make this room public": "Rendre ce salon public",
-    "Hide advanced": "Masquer les informations avancées",
-    "Show advanced": "Afficher les informations avancées",
+    "Hide advanced": "Masquer les paramètres avancés",
+    "Show advanced": "Afficher les paramètres avancés",
     "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Empêcher les utilisateurs d’autres serveurs d’accueil matrix de rejoindre ce salon (Ce paramètre ne peut pas être modifié plus tard !)",
     "To continue you need to accept the terms of this service.": "Pour continuer vous devez accepter les conditions de ce service.",
     "Document": "Document",
@@ -2373,7 +2373,7 @@
     "We’re excited to announce Riot is now Element!": "Nous sommes heureux d'annoncer que Riot est désormais Element !",
     "Learn more at <a>element.io/previously-riot</a>": "Plus d'infos sur <a>element.io/previously-riot</a>",
     "Search rooms": "Chercher des salons",
-    "User menu": "Menu d’utilisateur",
+    "User menu": "Menu utilisateur",
     "%(brand)s Web": "%(brand)s Web",
     "%(brand)s Desktop": "%(brand)s Desktop",
     "%(brand)s iOS": "%(brand)s iOS",
@@ -2856,13 +2856,13 @@
     "Send emotes as you in your active room": "Envoyer des réactions sous votre nom dans le salon actuel",
     "Send emotes as you in this room": "Envoyer des réactions sous votre nom dans ce salon",
     "See videos posted to your active room": "Voir les vidéos publiées dans votre salon actif",
-    "See videos posted to this room": "Voir les vidéos publiées dans ce salon",
+    "See videos posted to this room": "Voir les vidéos envoyées dans ce salon",
     "Send videos as you in your active room": "Envoie des vidéos sous votre nom dans votre salon actuel",
     "Send videos as you in this room": "Envoie des vidéos sous votre nom dans ce salon",
-    "See images posted to this room": "Voir les images publiées dans ce salon",
+    "See images posted to this room": "Voir les images envoyées dans ce salon",
     "See images posted to your active room": "Voir les images publiées dans votre salon actif",
     "See messages posted to your active room": "Voir les messages envoyés dans le salon actuel",
-    "See messages posted to this room": "Voir les messages publiés dans ce salon",
+    "See messages posted to this room": "Voir les messages envoyés dans ce salon",
     "Send messages as you in your active room": "Envoie des messages sous votre nom dans votre salon actif",
     "Send messages as you in this room": "Envoie des messages sous votre nom dans ce salon",
     "The <b>%(capability)s</b> capability": "La capacité <b>%(capability)s</b>",
@@ -2928,12 +2928,12 @@
     "Update %(brand)s": "Mettre à jour %(brand)s",
     "Enable desktop notifications": "Activer les notifications sur le bureau",
     "Don't miss a reply": "Ne ratez pas une réponse",
-    "See <b>%(msgtype)s</b> messages posted to your active room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans le salon actuel",
-    "See <b>%(msgtype)s</b> messages posted to this room": "Voir les messages de type <b>%(msgtype)s</b> publiés dans ce salon",
+    "See <b>%(msgtype)s</b> messages posted to your active room": "Voir les messages de type <b>%(msgtype)s</b> envoyés dans le salon actuel",
+    "See <b>%(msgtype)s</b> messages posted to this room": "Voir les messages de type <b>%(msgtype)s</b> envoyés dans ce salon",
     "Send <b>%(msgtype)s</b> messages as you in this room": "Envoie les messages de type <b>%(msgtype)s</b> sous votre nom dans ce salon",
     "Send <b>%(msgtype)s</b> messages as you in your active room": "Envoie des messages de type <b>%(msgtype)s</b> sous votre nom dans votre salon actif",
     "See general files posted to your active room": "Voir les fichiers postés dans votre salon actuel",
-    "See general files posted to this room": "Voir les fichiers postés dans ce salon",
+    "See general files posted to this room": "Voir les fichiers envoyés dans ce salon",
     "Send general files as you in your active room": "Envoyer des fichiers sous votre nom dans votre salon actif",
     "Send general files as you in this room": "Envoyer des fichiers sous votre nom dans ce salon",
     "Search (must be enabled)": "Recherche (si activée)",
@@ -3035,7 +3035,7 @@
     "See when the name changes in your active room": "Suivre les changements de nom dans le salon actif",
     "Change which room, message, or user you're viewing": "Changer le salon, message, ou la personne que vous visualisez",
     "Change which room you're viewing": "Changer le salon que vous êtes en train de lire",
-    "Remain on your screen while running": "Reste sur votre écran pendant l’exécution",
+    "Remain on your screen while running": "Reste sur votre écran pendant l’appel",
     "%(senderName)s has updated the widget layout": "%(senderName)s a mis à jour la disposition du widget",
     "Converts the DM to a room": "Transforme la conversation privée en salon",
     "Converts the room to a DM": "Transforme le salon en conversation privée",
@@ -3583,7 +3583,7 @@
     "You're the only admin of this space. Leaving it will mean no one has control over it.": "Vous êtes le seul administrateur de cet espace. En le quittant, plus personne n’aura le contrôle dessus.",
     "You won't be able to rejoin unless you are re-invited.": "Il vous sera impossible de revenir à moins d’y être réinvité.",
     "Search %(spaceName)s": "Rechercher %(spaceName)s",
-    "Leave specific rooms and spaces": "Laisser certains salons et espaces",
+    "Leave specific rooms and spaces": "Quitter certains salons et espaces",
     "Don't leave any": "Ne rien quitter",
     "Leave all rooms and spaces": "Quitter tous les salons et les espaces",
     "Want to add an existing space instead?": "Vous voulez plutôt ajouter un espace existant ?",
@@ -3678,5 +3678,37 @@
     "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s a désépinglé un message de ce salon. Voir tous les messages épinglés.",
     "%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s a désépinglé <a>un message</a> de ce salon. Voir tous les <b>messages épinglés</b>.",
     "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s a épinglé un message dans ce salon. Voir tous les messages épinglés.",
-    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s a épinglé <a>un message</a> dans ce salon. Voir tous les <b>messages épinglés</b>."
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s a épinglé <a>un message</a> dans ce salon. Voir tous les <b>messages épinglés</b>.",
+    "Some encryption parameters have been changed.": "Certains paramètres de chiffrement ont été changés.",
+    "Role in <RoomName/>": "Rôle dans <RoomName/>",
+    "Explore %(spaceName)s": "Explorer %(spaceName)s",
+    "Send a sticker": "Envoyer un autocollant",
+    "Reply to thread…": "Répondre au fil…",
+    "Reply to encrypted thread…": "Répondre au fil chiffré…",
+    "Add emoji": "Ajouter une émoticône",
+    "To join this Space, hide communities in your <a>preferences</a>": "Pour rejoindre cet espace, cachez les communautés dans vos <a>préférences</a>",
+    "To view this Space, hide communities in your <a>preferences</a>": "Pour voir cet espace, cachez les communautés dans vos <a>préférences</a>",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "Pour rejoindre %(communityName)s, changez pour les communautés dans vos <a>préférences</a>",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "Pour voir %(communityName)s, changez pour les communautés dans vos <a>préférences</a>",
+    "Private community": "Communauté privée",
+    "Public community": "Communauté publique",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s ont réagi avec %(content)s",
+    "Message": "Message",
+    "Joining space …": "Entrée dans l’espace…",
+    "Message didn't send. Click for info.": "Le message n’a pas été envoyé. Cliquer pour plus d’info.",
+    "Unknown failure": "Erreur inconnue",
+    "Failed to update the join rules": "Échec de mise-à-jour des règles pour rejoindre le salon",
+    "Select the roles required to change various parts of the space": "Sélectionner les rôles nécessaires pour modifier les différentes parties de l’espace",
+    "Change description": "Changer la description",
+    "Change main address for the space": "Changer l’adresse principale de l’espace",
+    "Change space name": "Changer le nom de l’espace",
+    "Change space avatar": "Changer l’avatar de l’espace",
+    "Upgrade anyway": "Mettre-à-jour quand même",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Ce salon se trouve dans certains espaces pour lesquels vous n’êtes pas administrateur. Dans ces espaces, l’ancien salon sera toujours afficher, mais un message sera affiché pour inciter les personnes à rejoindre le nouveau salon.",
+    "Before you upgrade": "Avant de mettre-à-jour",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Quiconque dans <spaceName/> peut trouver et rejoindre. Vous pouvez également choisir d’autres espaces.",
+    "To join a space you'll need an invite.": "Vous avez besoin d’une invitation pour rejoindre un espace.",
+    "You can also make Spaces from <a>communities</a>.": "Vous pouvez également créer des espaces à partir de <a>communautés</a>.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Montre temporairement les communautés au lieu des espaces pour cette session. Il ne sera plus possible de le faire dans un futur proche. Cela va recharger Element.",
+    "Display Communities instead of Spaces": "Afficher les communautés au lieu des espaces"
 }
diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json
index 6d08bcb266..ff17a44493 100644
--- a/src/i18n/strings/gl.json
+++ b/src/i18n/strings/gl.json
@@ -3702,5 +3702,35 @@
     "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s desafixou unha mensaxe desta sala. Mira tódalas mensaxes fixadas.",
     "%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s deafixou <a>unha mensaxe</a> desta sala. Mira tódalas <b>mensaxes fixadas</b>.",
     "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s fixou unha mensaxe nesta sala. Mira tódalas mensaxes fixadas.",
-    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fixou <a>unha mensaxe</a> nesta sala. Mira tódalas <b>mensaxes fixadas</b>."
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fixou <a>unha mensaxe</a> nesta sala. Mira tódalas <b>mensaxes fixadas</b>.",
+    "Some encryption parameters have been changed.": "Algún dos parámetros de cifrado foron cambiados.",
+    "Role in <RoomName/>": "Rol en <RoomName/>",
+    "Explore %(spaceName)s": "Explora %(spaceName)s",
+    "Send a sticker": "Enviar un adhesivo",
+    "Reply to thread…": "Responder á conversa…",
+    "Reply to encrypted thread…": "Responder á conversa cifrada…",
+    "Add emoji": "Engadir emoji",
+    "Unknown failure": "Fallo descoñecido",
+    "Failed to update the join rules": "Fallou a actualización das normas para unirse",
+    "Select the roles required to change various parts of the space": "Elexir os roles requeridos para cambiar varias partes do espazo",
+    "Change description": "Cambiar a descrición",
+    "Change main address for the space": "Cambiar o enderezo principal do espazo",
+    "Change space name": "Cambiar o nome do espazo",
+    "Change space avatar": "Cambiar o avatar do espazo",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Calquera en <spaceName/> pode atopar e unirse. Tamén podes elexir outros espazos.",
+    "Message didn't send. Click for info.": "Non se enviou a mensaxe. Click para info.",
+    "To join this Space, hide communities in your <a>preferences</a>": "Para unirte a este Espazo, oculta as comunidades nas túas <a>preferencias</a>",
+    "To view this Space, hide communities in your <a>preferences</a>": "Para ver este Espazo, oculta as comunidades nas túas <a>preferencias</a>",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "Para unirte a %(communityName)s, cambia a comunidades nas túas <a>preferencias</a>",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "Para ver %(communityName)s, cambia a comunidades nas túas <a>preferencias</a>",
+    "Private community": "Comunidade privada",
+    "Public community": "Comunidade pública",
+    "Message": "Mensaxe",
+    "Upgrade anyway": "Actualizar igualmente",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Esta sala está nalgúns espazos dos que non es admin. Nesos espazos, a antiga sala seguirá mostrándose, pero as persoas serán convidadas a unirse á nova.",
+    "Before you upgrade": "Antes de actualizar",
+    "To join a space you'll need an invite.": "Para unirte a un espazo precisas un convite.",
+    "You can also make Spaces from <a>communities</a>.": "Tamén podes crear Espazos a partir de <a>comunidades</a>.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "De xeito temporal, mostrar comunidades no lugar de Espazos durante esta sesión. Esta función vai ser eliminada en próximas versións. Reiniciará Element.",
+    "Display Communities instead of Spaces": "Mostrar Comunidades no lugar de Espazos"
 }
diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json
index df48f631cf..d5cd904767 100644
--- a/src/i18n/strings/hu.json
+++ b/src/i18n/strings/hu.json
@@ -3696,5 +3696,37 @@
     "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s kitűzött egy üzenetet ebben a szobában. Minden kitűzött üzenet megjelenítése.",
     "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s kitűzött <a>egy üzenetet</a> ebben a szobában. Minden <b>kitűzött üzenet</b> megjelenítése.",
     "Currently, %(count)s spaces have access|one": "Jelenleg a Térnek hozzáférése van",
-    "& %(count)s more|one": "és még %(count)s"
+    "& %(count)s more|one": "és még %(count)s",
+    "Some encryption parameters have been changed.": "Néhány titkosítási paraméter megváltozott.",
+    "Role in <RoomName/>": "Szerep itt: <RoomName/>",
+    "Explore %(spaceName)s": "%(spaceName)s feltérképezése",
+    "Send a sticker": "Matrica küldése",
+    "Reply to thread…": "Válasz az üzenetszálra…",
+    "Reply to encrypted thread…": "Válasz a titkosított üzenetszálra…",
+    "Add emoji": "Emodzsi hozzáadás",
+    "Unknown failure": "Ismeretlen hiba",
+    "Failed to update the join rules": "A csatlakozási szabályokat nem sikerült frissíteni",
+    "Select the roles required to change various parts of the space": "A tér bizonyos beállításainak megváltoztatásához szükséges szerep kiválasztása",
+    "Change description": "Leírás megváltoztatása",
+    "Change main address for the space": "Tér elsődleges címének megváltoztatása",
+    "Change space name": "Tér nevének megváltoztatása",
+    "Change space avatar": "Tér profilkép megváltoztatása",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "<spaceName/> téren bárki megtalálhatja és beléphet. Kiválaszthat más tereket is.",
+    "Message didn't send. Click for info.": "Az üzenet nincs elküldve. Kattintson az információkért.",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "%(communityName)s csatlakozáshoz álljon át közösségekre a <a>beállításokban</a>",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "%(communityName)s megjelenítéséhez álljon át közösségekre a <a>beállításokban</a>",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Ez a szoba olyan terekben is benne van amiben ön nem adminisztrátor. Ezekben a terekben a régi szoba jelenik meg és az emberek kapnak egy jelzést, hogy lépjenek be az újba.",
+    "To join this Space, hide communities in your <a>preferences</a>": "A Térbe való belépéshez rejtse el a közösségeket a <a>Beállításokban</a>",
+    "To view this Space, hide communities in your <a>preferences</a>": "A Tér megjelenítéséhez rejtse el a közösségeket a <a>Beállításokban</a>",
+    "Private community": "Zárt közösség",
+    "Public community": "Nyilvános közösség",
+    "Message": "Üzenet",
+    "Upgrade anyway": "Mindenképpen frissít",
+    "Before you upgrade": "Mielőtt frissítene",
+    "To join a space you'll need an invite.": "A térre való belépéshez meghívóra van szükség.",
+    "You can also make Spaces from <a>communities</a>.": "<a>Közösségből</a> is lehet Tereket készíteni.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Közösségek megjelenítése átmenetileg a terek helyett ebben a munkamenetben. Ez a lehetőség később kivezetésre kerül. Az Element újratöltődik.",
+    "Display Communities instead of Spaces": "Terek helyett inkább a közösségek megjelenítése",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s reagált: %(content)s",
+    "Joining space …": "Belépés a térbe…"
 }
diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json
index 1ea18cd5ac..86a210b049 100644
--- a/src/i18n/strings/it.json
+++ b/src/i18n/strings/it.json
@@ -244,7 +244,7 @@
     "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "Non potrai annullare questa modifica dato che stai promuovendo l'utente al tuo stesso grado.",
     "Unignore": "Non ignorare più",
     "Ignore": "Ignora",
-    "Mention": "Cita",
+    "Mention": "Menziona",
     "Invite": "Invita",
     "Unmute": "Togli silenzio",
     "and %(count)s others...|other": "e altri %(count)s ...",
@@ -1000,7 +1000,7 @@
     "Show avatar changes": "Mostra i cambi di avatar",
     "Show display name changes": "Mostra i cambi di nomi visualizzati",
     "Show read receipts sent by other users": "Mostra ricevute di lettura inviate da altri utenti",
-    "Show avatars in user and room mentions": "Mostra gli avatar nelle citazioni di utenti e stanze",
+    "Show avatars in user and room mentions": "Mostra gli avatar nelle menzioni di utenti e stanze",
     "Enable big emoji in chat": "Attiva gli emoji grandi in chat",
     "Send typing notifications": "Invia notifiche di scrittura",
     "Enable Community Filter Panel": "Attiva il pannello dei filtri di comunità",
@@ -1575,7 +1575,7 @@
     "Room %(name)s": "Stanza %(name)s",
     "Recent rooms": "Stanze recenti",
     "No identity server is configured so you cannot add an email address in order to reset your password in the future.": "Nessun server di identità configurato, perciò non puoi aggiungere un indirizzo email per ripristinare la tua password in futuro.",
-    "%(count)s unread messages including mentions.|one": "1 citazione non letta.",
+    "%(count)s unread messages including mentions.|one": "1 menzione non letta.",
     "%(count)s unread messages.|one": "1 messaggio non letto.",
     "Unread messages.": "Messaggi non letti.",
     "Show tray icon and minimize window to it on close": "Mostra icona in tray e usala alla chiusura della finestra",
@@ -2345,7 +2345,7 @@
     "Make this room low priority": "Rendi questa stanza a bassa priorità",
     "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Le stanze a bassa priorità vengono mostrate in fondo all'elenco stanze in una sezione dedicata",
     "Use default": "Usa predefinito",
-    "Mentions & Keywords": "Citazioni e parole chiave",
+    "Mentions & Keywords": "Menzioni e parole chiave",
     "Notification options": "Opzioni di notifica",
     "Favourited": "Preferito",
     "Forget Room": "Dimentica stanza",
@@ -3689,9 +3689,49 @@
     "Are you sure you want to add encryption to this public room?": "Vuoi veramente aggiungere la crittografia a questa stanza pubblica?",
     "Low bandwidth mode (requires compatible homeserver)": "Modalità a connessione lenta (richiede un homeserver compatibile)",
     "Multiple integration managers (requires manual setup)": "Gestori di integrazione multipli (richiede configurazione manuale)",
-    "Thread": "Argomento",
-    "Show threads": "Mostra argomenti",
+    "Thread": "Conversazione",
+    "Show threads": "Mostra conversazioni",
     "Threaded messaging": "Messaggi raggruppati",
     "The above, but in any room you are joined or invited to as well": "Quanto sopra, ma anche in qualsiasi stanza tu sia entrato o invitato",
-    "The above, but in <Room /> as well": "Quanto sopra, ma anche in <Room />"
+    "The above, but in <Room /> as well": "Quanto sopra, ma anche in <Room />",
+    "Currently, %(count)s spaces have access|one": "Attualmente, uno spazio ha accesso",
+    "& %(count)s more|one": "e altri %(count)s",
+    "Autoplay videos": "Auto-riproduci i video",
+    "Autoplay GIFs": "Auto-riproduci le GIF",
+    "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s ha tolto un messaggio ancorato da questa stanza. Vedi tutti i messaggi ancorati.",
+    "%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s ha tolto un <a>messaggio ancorato</a> da questa stanza. Vedi tutti i <b>messaggi ancorati</b>.",
+    "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s ha ancorato un messaggio a questa stanza. Vedi tutti i messaggi ancorati.",
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s ha ancorato <a>un messaggio</a> a questa stanza. Vedi tutti i <b>messaggi ancorati</b>.",
+    "Some encryption parameters have been changed.": "Alcuni parametri di crittografia sono stati modificati.",
+    "Role in <RoomName/>": "Ruolo in <RoomName/>",
+    "Explore %(spaceName)s": "Esplora %(spaceName)s",
+    "Send a sticker": "Invia uno sticker",
+    "Reply to thread…": "Rispondi alla conversazione…",
+    "Reply to encrypted thread…": "Rispondi alla conversazione cifrata…",
+    "Add emoji": "Aggiungi emoji",
+    "Unknown failure": "Errore sconosciuto",
+    "Failed to update the join rules": "Modifica delle regole di accesso fallita",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Chiunque in <spaceName/> può trovare ed entrare. Puoi selezionare anche altri spazi.",
+    "Select the roles required to change various parts of the space": "Seleziona i ruoli necessari per cambiare varie parti dello spazio",
+    "Change description": "Cambia descrizione",
+    "Change main address for the space": "Cambia indirizzo principale dello spazio",
+    "Change space name": "Cambia nome dello spazio",
+    "Change space avatar": "Cambia avatar dello spazio",
+    "Message didn't send. Click for info.": "Il messaggio non è stato inviato. Clicca per informazioni.",
+    "To join this Space, hide communities in your <a>preferences</a>": "Per entrare in questo spazio, nascondi le comunità nelle <a>preferenze</a>",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "Per entrare in %(communityName)s, passa alle comunità nelle <a>preferenze</a>",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "Per vedere %(communityName)s, passa alle comunità nelle <a>preferenze</a>",
+    "To view this Space, hide communities in your <a>preferences</a>": "Per vedere questo spazio, nascondi le comunità nelle <a>preferenze</a>",
+    "Private community": "Comunità privata",
+    "Public community": "Comunità pubblica",
+    "Message": "Messaggio",
+    "Upgrade anyway": "Aggiorna comunque",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Questa stanza è in alcuni spazi di cui non sei amministratore. In quegli spazi, la vecchia stanza verrà ancora mostrata, ma alla gente verrà chiesto di entrare in quella nuova.",
+    "Before you upgrade": "Prima di aggiornare",
+    "To join a space you'll need an invite.": "Per entrare in uno spazio ti serve un invito.",
+    "You can also make Spaces from <a>communities</a>.": "Puoi anche creare spazi dalle <a>comunità</a>.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Mostra temporaneamente le comunità invece degli spazi per questa sessione. Il supporto per questa azione verrà rimosso nel breve termine. Element verrà ricaricato.",
+    "Display Communities instead of Spaces": "Mostra le comunità invece degli spazi",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s ha reagito con %(content)s",
+    "Joining space …": "Ingresso nello spazio …"
 }
diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json
index 3ce4a121d2..e4d8b662e0 100644
--- a/src/i18n/strings/ja.json
+++ b/src/i18n/strings/ja.json
@@ -1791,8 +1791,8 @@
     "Cat": "猫",
     "Dog": "犬",
     "To be secure, do this in person or use a trusted way to communicate.": "安全を確保するため、1人でこれを行うか、または信頼できる方法で連携してください。",
-    "They don't match": "それらは一致しません",
-    "They match": "それらは一致します",
+    "They don't match": "異なる絵文字です",
+    "They match": "同じ絵文字です",
     "Cancelling…": "取り消し中…",
     "Waiting for your other session to verify…": "他のセッションによる検証を待っています…",
     "Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…": "他のセッション %(deviceName)s (%(deviceId)s) による検証を待っています…",
@@ -2542,5 +2542,13 @@
     "Only invited people can join.": "招待された人のみ参加できます。",
     "Private (invite only)": "プライベート (招待者のみ)",
     "Decide who can join %(roomName)s.": "%(roomName)s に参加できる人を設定します。",
-    "%(senderName)s invited %(targetName)s": "%(senderName)s が %(targetName)s を招待しました"
+    "%(senderName)s invited %(targetName)s": "%(senderName)s が %(targetName)s を招待しました",
+    "Verify other login": "他のログインを使用した検証",
+    "Accept on your other login…": "他のログインで了承してください…",
+    "Use Security Key": "セキュリティキーで検証",
+    "Use another login": "他の端末から検証",
+    "Verify your identity to access encrypted messages and prove your identity to others.": "あなたが本人であることを検証し、暗号化されたメッセージにアクセスします。",
+    "New? <a>Create account</a>": "<a>アカウント作成</a>",
+    "Are you sure you want to sign out?": "サインアウトしてよろしいですか?",
+    "Upgrade to %(hostSignupBrand)s": "%(hostSignupBrand)s にアップグレード"
 }
diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json
index 573ed6922a..dd70fb0814 100644
--- a/src/i18n/strings/nl.json
+++ b/src/i18n/strings/nl.json
@@ -25,7 +25,7 @@
     "Bans user with given id": "Verbant de persoon met de gegeven ID",
     "Call Timeout": "Oproeptime-out",
     "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Kan geen verbinding maken met de homeserver via HTTP wanneer er een HTTPS-URL in uw browserbalk staat. Gebruik HTTPS of <a>schakel onveilige scripts in</a>.",
-    "Change Password": "Wachtwoord veranderen",
+    "Change Password": "Wachtwoord wijzigen",
     "%(senderName)s changed their profile picture.": "%(senderName)s heeft een nieuwe profielfoto ingesteld.",
     "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s heeft het machtsniveau van %(powerLevelDiffText)s gewijzigd.",
     "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s heeft de kamernaam gewijzigd naar %(roomName)s.",
@@ -422,7 +422,7 @@
     "Delete Widget": "Widget verwijderen",
     "Who would you like to add to this community?": "Wie wil je toevoegen aan deze gemeenschap?",
     "Invite to Community": "Uitnodigen tot gemeenschap",
-    "Show these rooms to non-members on the community page and room list?": "Deze kamers tonen aan niet-leden op de gemeenschapspagina en openbare kamerlijst?",
+    "Show these rooms to non-members on the community page and room list?": "Deze kamers tonen aan niet-leden op de gemeenschapspagina en publieke kamersgids?",
     "Add rooms to the community": "Voeg kamers toe aan de gemeenschap",
     "Add to community": "Toevoegen aan gemeenschap",
     "Failed to invite the following users to %(groupId)s:": "Uitnodigen van volgende personen tot %(groupId)s is mislukt:",
@@ -695,7 +695,7 @@
     "Please set a password!": "Stel een wachtwoord in!",
     "You have successfully set a password!": "U heeft een wachtwoord ingesteld!",
     "An error occurred whilst saving your email notification preferences.": "Er is een fout opgetreden tijdens het opslaan van uw e-mailmeldingsvoorkeuren.",
-    "Explore Room State": "Gesprekstoestand verkennen",
+    "Explore Room State": "Kamertoestand ontdekken",
     "Source URL": "Bron-URL",
     "Messages sent by bot": "Berichten verzonden door een bot",
     "Filter results": "Resultaten filteren",
@@ -715,7 +715,7 @@
     "Remove %(name)s from the directory?": "%(name)s uit de catalogus verwijderen?",
     "%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s gebruikt veel geavanceerde browserfuncties, waarvan enkele niet (of slechts experimenteel) in uw browser beschikbaar zijn.",
     "Developer Tools": "Ontwikkelgereedschap",
-    "Explore Account Data": "Accountgegevens verkennen",
+    "Explore Account Data": "Accountgegevens ontdekken",
     "Remove from Directory": "Verwijderen uit catalogus",
     "Saturday": "Zaterdag",
     "Remember, you can always set an email address in user settings if you change your mind.": "Onthoud dat u altijd nog een e-mailadres kunt instellen in de gebruikersinstellingen.",
@@ -1052,8 +1052,8 @@
     "Voice & Video": "Spraak & video",
     "Room information": "Gespreksinformatie",
     "Internal room ID:": "Interne gespreks-ID:",
-    "Room version": "Gespreksversie",
-    "Room version:": "Gespreksversie:",
+    "Room version": "Kamerversie",
+    "Room version:": "Kamerversie:",
     "Developer options": "Ontwikkelaarsopties",
     "Open Devtools": "Ontwikkelgereedschap openen",
     "Room Addresses": "Gespreksadressen",
@@ -1134,7 +1134,7 @@
     "Failed to upgrade room": "Kamerupgrade mislukt",
     "The room upgrade could not be completed": "Het upgraden van de kamer kon niet worden voltooid",
     "Upgrade this room to version %(version)s": "Upgrade de kamer naar versie %(version)s",
-    "Upgrade Room Version": "Gespreksversie upgraden",
+    "Upgrade Room Version": "Kamerversie upgraden",
     "Create a new room with the same name, description and avatar": "Een nieuw kamer aanmaken met dezelfde naam, beschrijving en afbeelding",
     "Update any local room aliases to point to the new room": "Alle lokale gespreksbijnamen naar het nieuwe gesprek laten verwijzen",
     "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Personen verhinderen om aan de oude versie van de kamer bij te dragen en plaats een bericht te dat de personen verwijst naar de nieuwe kamer",
@@ -1177,7 +1177,7 @@
     "Homeserver URL": "Thuisserver-URL",
     "Identity Server URL": "Identiteitsserver-URL",
     "Free": "Gratis",
-    "Join millions for free on the largest public server": "Neem deel aan de grootste openbare server met miljoenen anderen",
+    "Join millions for free on the largest public server": "Neem deel aan de grootste publieke server samen met miljoenen anderen",
     "Premium": "Premium",
     "Premium hosting for organisations <a>Learn more</a>": "Premium hosting voor organisaties <a>Lees meer</a>",
     "Other": "Overige",
@@ -1238,8 +1238,8 @@
     "Please supply a https:// or http:// widget URL": "Voer een https://- of http://-widget-URL in",
     "You cannot modify widgets in this room.": "U kunt de widgets in deze kamer niet aanpassen.",
     "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s heeft de uitnodiging aan %(targetDisplayName)s toe te treden tot deze kamer ingetrokken.",
-    "Upgrade this room to the recommended room version": "Upgrade dit gesprek naar de aanbevolen gespreksversie",
-    "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "Dit gesprek draait op groepsgespreksversie <roomVersion />, die door deze homeserver als <i>onstabiel</i> is gemarkeerd.",
+    "Upgrade this room to the recommended room version": "Upgrade deze kamer naar de aanbevolen kamerversie",
+    "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "Deze kamer draait op kamerversie <roomVersion />, die door deze server als <i>onstabiel</i> is gemarkeerd.",
     "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgraden zal de huidige versie van dit gesprek sluiten, en onder dezelfde naam een geüpgraded versie starten.",
     "Failed to revoke invite": "Intrekken van uitnodiging is mislukt",
     "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Kon de uitnodiging niet intrekken. De server ondervindt mogelijk een tijdelijk probleem, of u heeft niet het recht de uitnodiging in te trekken.",
@@ -1255,7 +1255,7 @@
     "The homeserver may be unavailable or overloaded.": "De homeserver is mogelijk onbereikbaar of overbelast.",
     "You have %(count)s unread notifications in a prior version of this room.|other": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.",
     "You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.",
-    "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u al dan niet de afbeeldingen voor recent bekeken kamers (boven de kamerlijst) gebruikt",
+    "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u al dan niet de afbeeldingen voor recent bekeken kamers (boven de lijst met kamers) gebruikt",
     "Replying With Files": "Beantwoorden met bestanden",
     "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Het is momenteel niet mogelijk met een bestand te antwoorden. Wil je dit bestand uploaden zonder te antwoorden?",
     "The file '%(fileName)s' failed to upload.": "Het bestand ‘%(fileName)s’ kon niet geüpload worden.",
@@ -1454,7 +1454,7 @@
     "Send read receipts for messages (requires compatible homeserver to disable)": "Verstuur leesbevestigingen voor berichten (uitschakelen vereist een compatibele thuisserver)",
     "Accept <policyLink /> to continue:": "Aanvaard <policyLink /> om door te gaan:",
     "ID": "ID",
-    "Public Name": "Openbare naam",
+    "Public Name": "Publieke naam",
     "Change identity server": "Identiteitsserver wisselen",
     "Disconnect from the identity server <current /> and connect to <new /> instead?": "Verbinding met identiteitsserver <current /> verbreken en in plaats daarvan verbinden met <new />?",
     "Disconnect identity server": "Verbinding met identiteitsserver verbreken",
@@ -1514,7 +1514,7 @@
     "Find a room…": "Zoek een gesprek…",
     "Find a room… (e.g. %(exampleRoom)s)": "Zoek een gesprek… (bv. %(exampleRoom)s)",
     "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Als u de kamer niet kunt vinden is het mogelijk privé, vraag dan om een uitnodiging of <a>maak een nieuwe kamer aan</a>.",
-    "Explore rooms": "Kamersgids",
+    "Explore rooms": "Ontdek kamers",
     "Show previews/thumbnails for images": "Miniaturen voor afbeeldingen tonen",
     "Clear cache and reload": "Cache wissen en herladen",
     "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "U staat op het punt 1 bericht door %(user)s te verwijderen. Dit kan niet ongedaan gemaakt worden. Wilt u doorgaan?",
@@ -1558,7 +1558,7 @@
     "Trust": "Vertrouwen",
     "Custom (%(level)s)": "Aangepast (%(level)s)",
     "Error upgrading room": "Upgraden van gesprek mislukt",
-    "Double check that your server supports the room version chosen and try again.": "Ga nogmaals na dat de server de gekozen gespreksversie ondersteunt, en probeer het dan opnieuw.",
+    "Double check that your server supports the room version chosen and try again.": "Ga nogmaals na dat de server de gekozen kamerversie ondersteunt, en probeer het dan opnieuw.",
     "Verifies a user, session, and pubkey tuple": "Verifieert de combinatie van persoon, sessie en publieke sleutel",
     "Unknown (user, session) pair:": "Onbekende combinatie persoon en sessie:",
     "Session already verified!": "Sessie al geverifieerd!",
@@ -1698,7 +1698,7 @@
     "Session ID:": "Sessie-ID:",
     "Session key:": "Sessiesleutel:",
     "Message search": "Berichten zoeken",
-    "A session's public name is visible to people you communicate with": "De openbare naam van een sessie is zichtbaar voor de personen waarmee u communiceert",
+    "A session's public name is visible to people you communicate with": "De publieke naam van een sessie is zichtbaar voor de personen waarmee u communiceert",
     "This room is bridging messages to the following platforms. <a>Learn more.</a>": "Dit gesprek wordt overbrugd naar de volgende platformen. <a>Lees meer</a>",
     "This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "Dit gesprek wordt niet overbrugd naar andere platformen. <a>Lees meer.</a>",
     "Bridges": "Bruggen",
@@ -2319,17 +2319,17 @@
     "We couldn't log you in": "We konden u niet inloggen",
     "Room Info": "Gespreksinfo",
     "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "Matrix.org is de grootste publieke homeserver van de wereld, en dus een goede plek voor de meeste.",
-    "Explore Public Rooms": "Ontdek publieke kamers",
+    "Explore Public Rooms": "Publieke kamers ontdekken",
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Privékamers zijn alleen zichtbaar en toegankelijk met een uitnodiging. Publieke kamers zijn zichtbaar en toegankelijk voor iedereen in deze gemeenschap.",
     "This room is public": "Dit gesprek is openbaar",
     "Show previews of messages": "Voorvertoning van berichten inschakelen",
     "Show message previews for reactions in all rooms": "Berichtvoorbeelden voor reacties in alle kamers tonen",
-    "Explore public rooms": "Ontdek publieke kamers",
+    "Explore public rooms": "Publieke kamers ontdekken",
     "Leave Room": "Gesprek verlaten",
     "Room options": "Gesprekopties",
     "Start a conversation with someone using their name, email address or username (like <userId/>).": "Start een gesprek met iemand door hun naam, e-mailadres of inlognaam (zoals <userId/>) te typen.",
-    "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Berichten hier zijn eind-tot-eind versleuteld. Verifieer %(displayName)s op hun profiel - klik op hun afbeelding.",
-    "%(creator)s created this DM.": "%(creator)s maakte deze DM.",
+    "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Berichten hier zijn eind-tot-eind-versleuteld. Verifieer %(displayName)s op hun profiel - klik op hun afbeelding.",
+    "%(creator)s created this DM.": "%(creator)s maakte deze directe chat.",
     "Switch to dark mode": "Naar donkere modus wisselen",
     "Switch to light mode": "Naar lichte modus wisselen",
     "Appearance": "Weergave",
@@ -2343,17 +2343,17 @@
     "Favourited": "Favoriet",
     "Forget Room": "Gesprek vergeten",
     "Notification options": "Meldingsinstellingen",
-    "Use default": "Gebruik standaardinstelling",
+    "Use default": "Standaardinstelling gebruiken",
     "Show %(count)s more|one": "Toon %(count)s meer",
     "Show %(count)s more|other": "Toon %(count)s meer",
     "Show rooms with unread messages first": "Kamers met ongelezen berichten als eerste tonen",
     "%(count)s results|one": "%(count)s resultaten",
     "%(count)s results|other": "%(count)s resultaten",
-    "Explore all public rooms": "Verken alle openbare gespreken",
+    "Explore all public rooms": "Alle publieke kamers ontdekken",
     "Start a new chat": "Nieuw gesprek beginnen",
     "Can't see what you’re looking for?": "Niet kunnen vinden waar u naar zocht?",
     "Custom Tag": "Aangepast label",
-    "Explore community rooms": "Gemeenschapskamers verkennen",
+    "Explore community rooms": "Gemeenschapskamers ontdekken",
     "Start a Conversation": "Begin een gesprek",
     "Show Widgets": "Widgets tonen",
     "Hide Widgets": "Widgets verbergen",
@@ -2459,7 +2459,7 @@
     "Add image (optional)": "Afbeelding toevoegen (niet vereist)",
     "Enter name": "Naam invoeren",
     "What's the name of your community or team?": "Welke naam heeft uw gemeenschap of team?",
-    "You can change this later if needed.": "Indien nodig kunt u dit later nog veranderen.",
+    "You can change this later if needed.": "Indien nodig kunt u dit later nog wijzigen.",
     "Community ID: +<localpart />:%(domain)s": "Gemeenschaps-ID: +<localpart />:%(domain)s",
     "Reason (optional)": "Reden (niet vereist)",
     "Send %(count)s invites|one": "Stuur %(count)s uitnodiging",
@@ -2474,7 +2474,7 @@
     "Matrix": "Matrix",
     "Are you sure you want to remove <b>%(serverName)s</b>": "Weet u zeker dat u <b>%(serverName)s</b> wilt verwijderen",
     "Your server": "Uw server",
-    "Can't find this server or its room list": "Kan de server of haar kamerlijst niet vinden",
+    "Can't find this server or its room list": "Kan de server of haar kamergids niet vinden",
     "Looks good": "Ziet er goed uit",
     "Enter a server name": "Geef een servernaam",
     "Continue with %(provider)s": "Doorgaan met %(provider)s",
@@ -2757,7 +2757,7 @@
     "Got an account? <a>Sign in</a>": "Heeft u een account? <a>Inloggen</a>",
     "Failed to find the general chat for this community": "De algemene chat voor deze gemeenschap werd niet gevonden",
     "Filter rooms and people": "Gespreken en personen filteren",
-    "Explore rooms in %(communityName)s": "Ontdek de kamers van %(communityName)s",
+    "Explore rooms in %(communityName)s": "Kamers van %(communityName)s ontdekken",
     "delete the address.": "het adres verwijderen.",
     "Delete the room address %(alias)s and remove %(name)s from the directory?": "Het kameradres %(alias)s en %(name)s uit de gids verwijderen?",
     "You have no visible notifications.": "U hebt geen zichtbare meldingen.",
@@ -2880,7 +2880,7 @@
     "Preparing to download logs": "Klaarmaken om logs te downloaden",
     "Matrix rooms": "Matrix-kamers",
     "%(networkName)s rooms": "%(networkName)s kamers",
-    "Enter the name of a new server you want to explore.": "Voer de naam in van een nieuwe server die u wilt verkennen.",
+    "Enter the name of a new server you want to explore.": "Voer de naam in van een nieuwe server die u wilt ontdekken.",
     "Remove server": "Server verwijderen",
     "All rooms": "Alle kamers",
     "Windows": "Windows",
@@ -2971,7 +2971,7 @@
     "Value": "Waarde",
     "Setting ID": "Instellingen-ID",
     "Failed to save settings": "Kan geen instellingen opslaan",
-    "Settings Explorer": "Instellingen Ontdekken",
+    "Settings Explorer": "Instellingen ontdekken",
     "Show chat effects (animations when receiving e.g. confetti)": "Effecten tonen (animaties bij ontvangst bijv. confetti)",
     "Jump to the bottom of the timeline when you send a message": "Naar de onderkant van de tijdlijn springen wanneer u een bericht verstuurd",
     "Original event source": "Originele gebeurtenisbron",
@@ -2983,8 +2983,8 @@
     "Inviting...": "Uitnodigen...",
     "Invite by username": "Op inlognaam uitnodigen",
     "Invite your teammates": "Uw teamgenoten uitnodigen",
-    "Failed to invite the following users to your space: %(csvUsers)s": "Het uitnodigen van de volgende personen voor uw space is mislukt: %(csvUsers)s",
-    "A private space for you and your teammates": "Een privé space voor u en uw teamgenoten",
+    "Failed to invite the following users to your space: %(csvUsers)s": "Het uitnodigen van de volgende personen voor uw ruimte is mislukt: %(csvUsers)s",
+    "A private space for you and your teammates": "Een privéruimte voor u en uw teamgenoten",
     "Me and my teammates": "Ik en mijn teamgenoten",
     "A private space just for you": "Een privé space alleen voor u",
     "Just Me": "Alleen Ik",
@@ -2994,7 +2994,7 @@
     "At the moment only you can see it.": "Op dit moment kan u deze alleen zien.",
     "Creating rooms...": "Kamers aanmaken...",
     "Skip for now": "Voorlopig overslaan",
-    "Failed to create initial space rooms": "Het maken van de space kamers is mislukt",
+    "Failed to create initial space rooms": "Het maken van de ruimte kamers is mislukt",
     "Room name": "Gespreksnaam",
     "Support": "Ondersteuning",
     "Random": "Willekeurig",
@@ -3005,7 +3005,7 @@
     "<inviter/> invited you to <name/>": "<inviter/> heeft u uitgenodigd voor <name/>",
     "%(count)s members|other": "%(count)s personen",
     "%(count)s members|one": "%(count)s persoon",
-    "Your server does not support showing space hierarchies.": "Uw server heeft geen ondersteuning voor het weergeven van space indelingen.",
+    "Your server does not support showing space hierarchies.": "Uw server heeft geen ondersteuning voor het weergeven van ruimte-indelingen.",
     "Default Rooms": "Standaard Gesprekken",
     "Add existing rooms & spaces": "Bestaande gesprekken en spaces toevoegen",
     "Accept Invite": "Uitnodiging Accepteren",
@@ -3019,71 +3019,71 @@
     "Remove from Space": "Van space verwijderen",
     "Undo": "Ongedaan maken",
     "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please <a>contact your service administrator</a> to continue using the service.": "Uw bericht is niet verstuurd, omdat deze homeserver is geblokkeerd door zijn beheerder. Gelieve <a>contact op te nemen met uw beheerder</a> om de dienst te blijven gebruiken.",
-    "Are you sure you want to leave the space '%(spaceName)s'?": "Weet u zeker dat u de space '%(spaceName)s' wilt verlaten?",
-    "This space is not public. You will not be able to rejoin without an invite.": "Deze space is niet openbaar. Zonder uitnodiging zult u niet opnieuw kunnen toetreden.",
+    "Are you sure you want to leave the space '%(spaceName)s'?": "Weet u zeker dat u de ruimte '%(spaceName)s' wilt verlaten?",
+    "This space is not public. You will not be able to rejoin without an invite.": "Deze ruimte is niet openbaar. Zonder uitnodiging zult u niet opnieuw kunnen toetreden.",
     "Start audio stream": "Audiostream starten",
     "Failed to start livestream": "Starten van livestream is mislukt",
     "Unable to start audio streaming.": "Kan audiostream niet starten.",
     "Save Changes": "Wijzigingen opslaan",
     "Saving...": "Opslaan...",
     "View dev tools": "Bekijk dev tools",
-    "Leave Space": "Space verlaten",
+    "Leave Space": "Ruimte verlaten",
     "Make this space private": "Maak deze space privé",
-    "Failed to save space settings.": "Het opslaan van de space-instellingen is mislukt.",
-    "Space settings": "Space-instellingen",
-    "Edit settings relating to your space.": "Bewerk instellingen gerelateerd aan uw space.",
-    "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Nodig iemand uit per naam, inlognaam (zoals <userId/>) of <a>deel deze space</a>.",
-    "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Nodig iemand uit per naam, e-mailadres, inlognaam (zoals <userId/>) of <a>deel deze space</a>.",
-    "Unnamed Space": "Naamloze space",
+    "Failed to save space settings.": "Het opslaan van de ruimte-instellingen is mislukt.",
+    "Space settings": "Ruimte-instellingen",
+    "Edit settings relating to your space.": "Bewerk instellingen gerelateerd aan uw ruimte.",
+    "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Nodig iemand uit per naam, inlognaam (zoals <userId/>) of <a>deel deze ruimte</a>.",
+    "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Nodig iemand uit per naam, e-mail, inlognaam (zoals <userId/>) of <a>deel deze ruimte</a>.",
+    "Unnamed Space": "Naamloze ruimte",
     "Invite to %(spaceName)s": "Voor %(spaceName)s uitnodigen",
     "Failed to add rooms to space": "Het toevoegen van gesprekken aan de space is mislukt",
     "Apply": "Toepassen",
     "Applying...": "Toepassen...",
     "Create a new room": "Nieuw gesprek aanmaken",
     "Don't want to add an existing room?": "Wilt u geen bestaand gesprek toevoegen?",
-    "Spaces": "Spaces",
+    "Spaces": "Ruimtes",
     "Filter your rooms and spaces": "Gesprekken en spaces filteren",
     "Add existing spaces/rooms": "Bestaande spaces/gesprekken toevoegen",
-    "Space selection": "Space-selectie",
-    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "U kunt deze wijziging niet ongedaan maken, omdat u uzelf rechten ontneemt. Als u de laatste bevoegde persoon in de space bent zal het onmogelijk zijn om weer rechten te krijgen.",
+    "Space selection": "Ruimte-selectie",
+    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "U kunt deze wijziging niet ongedaan maken, omdat u uzelf rechten ontneemt. Als u de laatste bevoegde persoon in de ruimte bent zal het onmogelijk zijn om weer rechten te krijgen.",
     "Empty room": "Leeg gesprek",
     "Suggested Rooms": "Gespreksuggesties",
     "Explore space rooms": "Space-gesprekken ontdekken",
-    "You do not have permissions to add rooms to this space": "U hebt geen toestemming om kamers toe te voegen in deze space",
+    "You do not have permissions to add rooms to this space": "U hebt geen toestemming om kamers toe te voegen in deze ruimte",
     "Add existing room": "Bestaande kamers toevoegen",
-    "You do not have permissions to create new rooms in this space": "U hebt geen toestemming om kamers te maken in deze space",
+    "You do not have permissions to create new rooms in this space": "U hebt geen toestemming om kamers te maken in deze ruimte",
     "Send message": "Bericht versturen",
-    "Invite to this space": "Uitnodigen voor deze space",
+    "Invite to this space": "Voor deze ruimte uitnodigen",
     "Your message was sent": "Uw bericht is verstuurd",
     "Encrypting your message...": "Uw bericht versleutelen...",
     "Sending your message...": "Uw bericht versturen...",
     "Spell check dictionaries": "Spellingscontrole woordenboeken",
-    "Space options": "Space-opties",
+    "Space options": "Ruimte-opties",
     "Space Home": "Space Thuis",
     "New room": "Nieuw gesprek",
-    "Leave space": "Space verlaten",
+    "Leave space": "Ruimte verlaten",
     "Invite people": "Personen uitnodigen",
-    "Share your public space": "Deel uw openbare space",
+    "Share your public space": "Deel uw publieke ruimte",
     "Invite members": "Leden uitnodigen",
     "Invite by email or username": "Uitnodigen per e-mail of gebruikersnaam",
     "Share invite link": "Deel uitnodigingskoppeling",
     "Click to copy": "Klik om te kopiëren",
-    "Collapse space panel": "Space-paneel invouwen",
-    "Expand space panel": "Space-paneel uitvouwen",
+    "Collapse space panel": "Ruimte-paneel invouwen",
+    "Expand space panel": "Ruimte-paneel uitvouwen",
     "Creating...": "Aanmaken...",
     "You can change these at any point.": "U kan dit op elk moment aanpassen.",
     "Give it a photo, name and description to help you identify it.": "Geef het een foto, naam en omschrijving om u te helpen het te herkennen.",
-    "Your private space": "Uw privé space",
-    "Your public space": "Uw openbare space",
+    "Your private space": "Uw privéruimte",
+    "Your public space": "Uw publieke ruimte",
     "You can change this later": "U kan dit later aanpassen",
     "Invite only, best for yourself or teams": "Alleen op uitnodiging, geschikt voor uzelf of teams",
     "Private": "Privé",
-    "Open space for anyone, best for communities": "Openbare space voor iedereen, geschikt voor gemeenschappen",
+    "Open space for anyone, best for communities": "Publieke ruimte voor iedereen, geschikt voor gemeenschappen",
     "Public": "Openbaar",
     "Spaces are new ways to group rooms and people. To join an existing space you’ll need an invite": "Spaces is een nieuwe manier van groeperen van gesprekken en personen. Om deel te nemen aan een bestaande space heeft u een uitnodiging nodig",
-    "Create a space": "Space aanmaken",
+    "Create a space": "Ruimte maken",
     "Delete": "Verwijderen",
-    "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Niet compatibel met Gemeenschappen, Gemeenschappen v2 en Aangepaste Labels. Vereist een geschikte homeserver voor sommige functies.",
+    "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Ruimtes prototype. Niet compatibel met Gemeenschappen, Gemeenschappen v2 en Aangepaste Labels. Vereist een geschikte homeserver voor sommige functies.",
     "This homeserver has been blocked by it's administrator.": "Deze homeserver is geblokkeerd door zijn beheerder.",
     "This homeserver has been blocked by its administrator.": "Deze homeserver is geblokkeerd door uw beheerder.",
     "Already in call": "Al in de oproep",
@@ -3092,15 +3092,15 @@
     "Verify with another session": "Verifieer met een andere sessie",
     "We'll create rooms for each of them. You can add more later too, including already existing ones.": "We zullen voor elk een kamer maken. U kunt er later meer toevoegen, inclusief al bestaande kamers.",
     "Let's create a room for each of them. You can add more later too, including already existing ones.": "Laten we voor elk een gesprek maken. U kunt er later meer toevoegen, inclusief al bestaande gesprekken.",
-    "Make sure the right people have access. You can invite more later.": "Controleer of de juiste mensen toegang hebben. U kunt later meer mensen uitnodigen.",
-    "A private space to organise your rooms": "Een privé space om uw kamers te organiseren",
+    "Make sure the right people have access. You can invite more later.": "Controleer of de juiste personen toegang hebben. U kunt later meer personen uitnodigen.",
+    "A private space to organise your rooms": "Een privéruimte om uw kamers te organiseren",
     "Just me": "Alleen ik",
-    "Make sure the right people have access to %(name)s": "Controleer of de juiste mensen toegang hebben tot %(name)s",
+    "Make sure the right people have access to %(name)s": "Controleer of de juiste personen toegang hebben tot %(name)s",
     "Go to my first room": "Ga naar mijn eerste gesprek",
     "It's just you at the moment, it will be even better with others.": "Het is alleen u op dit moment, het zal nog beter zijn met anderen.",
     "Share %(name)s": "Deel %(name)s",
-    "Private space": "Privé space",
-    "Public space": "Openbare space",
+    "Private space": "Privéruimte",
+    "Public space": "Publieke ruimte",
     "<inviter/> invites you": "<inviter/> nodigt u uit",
     "Search names and description": "Zoek in namen en beschrijvingen",
     "Create room": "Gesprek aanmaken",
@@ -3126,10 +3126,10 @@
     "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.": "Normaal gesproken heeft dit alleen invloed op het verwerken van het gesprek op de server. Als u problemen ervaart met %(brand)s, stuur dan een bugmelding.",
     "Invite to %(roomName)s": "Uitnodiging voor %(roomName)s",
     "Edit devices": "Apparaten bewerken",
-    "Invite People": "Mensen uitnodigen",
+    "Invite People": "Personen uitnodigen",
     "Invite with email or username": "Uitnodigen per e-mail of inlognaam",
     "You can change these anytime.": "U kan dit elk moment nog aanpassen.",
-    "Add some details to help people recognise it.": "Voeg details toe zodat mensen het herkennen.",
+    "Add some details to help people recognise it.": "Voeg details toe zodat personen het herkennen.",
     "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Spaces zijn een nieuwe manier voor het groeperen van gesprekken. Voor deelname aan een bestaande space heeft u een uitnodiging nodig.",
     "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Van %(deviceName)s (%(deviceId)s) op %(ip)s",
     "Check your devices": "Controleer uw apparaten",
@@ -3193,36 +3193,36 @@
     "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s leden inclusief %(commaSeparatedMembers)s",
     "Including %(commaSeparatedMembers)s": "Inclusief %(commaSeparatedMembers)s",
     "View all %(count)s members|one": "1 lid bekijken",
-    "View all %(count)s members|other": "Bekijk alle %(count)s leden",
+    "View all %(count)s members|other": "Bekijk alle %(count)s personen",
     "Failed to send": "Verzenden is mislukt",
     "Enter your Security Phrase a second time to confirm it.": "Voor uw veiligheidswachtwoord een tweede keer in om het te bevestigen.",
-    "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Kies een gesprek om hem toe te voegen. Dit is een space voor u, niemand zal hiervan een melding krijgen. U kan er later meer toevoegen.",
+    "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Kies een kamer of gesprek om hem toe te voegen. Dit is een ruimte voor u, niemand zal hiervan een melding krijgen. U kan er later meer toevoegen.",
     "What do you want to organise?": "Wat wilt u organiseren?",
-    "Filter all spaces": "Alle spaces filteren",
+    "Filter all spaces": "Alle ruimtes filteren",
     "Delete recording": "Opname verwijderen",
     "Stop the recording": "Opname stoppen",
-    "%(count)s results in all spaces|one": "%(count)s resultaat in alle spaces",
-    "%(count)s results in all spaces|other": "%(count)s resultaten in alle spaces",
+    "%(count)s results in all spaces|one": "%(count)s resultaat in alle ruimtes",
+    "%(count)s results in all spaces|other": "%(count)s resultaten in alle ruimtes",
     "You have no ignored users.": "U heeft geen persoon genegeerd.",
     "Play": "Afspelen",
     "Pause": "Pauze",
     "<b>This is an experimental feature.</b> For now, new users receiving an invite will have to open the invite on <link/> to actually join.": "<b>Dit is een experimentele functie.</b> Voorlopig moeten nieuwe personen die een uitnodiging krijgen de <link/> gebruiken om daadwerkelijk deel te nemen.",
-    "To join %(spaceName)s, turn on the <a>Spaces beta</a>": "Om aan %(spaceName)s deel te nemen moet u de <a>Spaces beta</a> inschakelen",
-    "To view %(spaceName)s, turn on the <a>Spaces beta</a>": "Om %(spaceName)s te bekijken moet u de <a>Spaces beta</a> inschakelen",
+    "To join %(spaceName)s, turn on the <a>Spaces beta</a>": "Om aan %(spaceName)s deel te nemen moet u de <a>ruimtes beta</a> inschakelen",
+    "To view %(spaceName)s, turn on the <a>Spaces beta</a>": "Om %(spaceName)s te bekijken moet u de <a>ruimtes beta</a> inschakelen",
     "Select a room below first": "Start met selecteren van een gesprek hieronder",
-    "Communities are changing to Spaces": "Gemeenschappen worden vervangen door Spaces",
+    "Communities are changing to Spaces": "Gemeenschappen worden vervangen door ruimtes",
     "Join the beta": "Beta inschakelen",
     "Leave the beta": "Beta verlaten",
     "Beta": "Beta",
     "Tap for more info": "Klik voor meer info",
-    "Spaces is a beta feature": "Spaces zijn in beta",
+    "Spaces is a beta feature": "Ruimtes zijn in beta",
     "Want to add a new room instead?": "Wilt u anders een nieuw gesprek toevoegen?",
     "Adding rooms... (%(progress)s out of %(count)s)|one": "Gesprek toevoegen...",
     "Adding rooms... (%(progress)s out of %(count)s)|other": "Kamers toevoegen... (%(progress)s van %(count)s)",
     "Not all selected were added": "Niet alle geselecteerden zijn toegevoegd",
     "You can add existing spaces to a space.": "U kunt bestaande spaces toevoegen aan een space.",
     "Feeling experimental?": "Zin in een experiment?",
-    "You are not allowed to view this server's rooms list": "U heeft geen toegang tot deze server zijn kamerlijst",
+    "You are not allowed to view this server's rooms list": "U heeft geen toegang tot deze server zijn kamergids",
     "Error processing voice message": "Fout bij verwerking spraakbericht",
     "We didn't find a microphone on your device. Please check your settings and try again.": "We hebben geen microfoon gevonden op uw apparaat. Controleer uw instellingen en probeer het opnieuw.",
     "No microphone found": "Geen microfoon gevonden",
@@ -3232,15 +3232,15 @@
     "Your access token gives full access to your account. Do not share it with anyone.": "Uw toegangstoken geeft u toegang to uw account. Deel hem niet met anderen.",
     "Access Token": "Toegangstoken",
     "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Spaces zijn de nieuwe manier om gesprekken en personen te groeperen. Om aan een bestaande space deel te nemen heeft u een uitnodiging nodig.",
-    "Please enter a name for the space": "Vul een naam in voor deze space",
+    "Please enter a name for the space": "Vul een naam in voor deze ruimte",
     "Connecting": "Verbinden",
     "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Peer-to-peer voor 1op1 oproepen toestaan (als u dit inschakelt kunnen andere personen mogelijk uw ipadres zien)",
     "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "De beta is beschikbaar voor web, desktop en Android. Sommige functies zijn nog niet beschikbaar op uw homeserver.",
     "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "U kunt de beta elk moment verlaten via instellingen of door op de beta badge hierboven te klikken.",
-    "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s zal herladen met Spaces ingeschakeld. Gemeenschappen en labels worden verborgen.",
+    "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s zal herladen met Ruimtes ingeschakeld. Gemeenschappen en labels worden verborgen.",
     "Beta available for web, desktop and Android. Thank you for trying the beta.": "De beta is beschikbaar voor web, desktop en Android. Bedankt dat u de beta wilt proberen.",
     "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s zal herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.",
-    "Spaces are a new way to group rooms and people.": "Spaces zijn de nieuwe manier om kamers en personen te groeperen.",
+    "Spaces are a new way to group rooms and people.": "Ruimtes zijn de nieuwe manier om kamers en personen te groeperen.",
     "Message search initialisation failed": "Zoeken in berichten opstarten is mislukt",
     "Spaces are a beta feature.": "Spaces zijn een beta functie.",
     "Search names and descriptions": "Namen en beschrijvingen zoeken",
@@ -3252,12 +3252,12 @@
     "Beta feedback": "Beta feedback",
     "Add reaction": "Reactie toevoegen",
     "Send and receive voice messages": "Stuur en ontvang spraakberichten",
-    "Your feedback will help make spaces better. The more detail you can go into, the better.": "Uw feedback maakt spaces beter. Hoe meer details u kan geven, des te beter.",
-    "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Als u de pagina nu verlaat zal %(brand)s herladen met Spaces uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.",
-    "Space Autocomplete": "Space Autocomplete",
-    "Go to my space": "Ga naar mijn space",
+    "Your feedback will help make spaces better. The more detail you can go into, the better.": "Uw feedback maakt ruimtes beter. Hoe meer details u kunt geven, hoe beter.",
+    "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Als u de pagina nu verlaat zal %(brand)s herladen met Ruimtes uitgeschakeld. Gemeenschappen en labels zullen weer zichtbaar worden.",
+    "Space Autocomplete": "Ruimte autocomplete",
+    "Go to my space": "Ga naar mijn ruimte",
     "sends space invaders": "verstuur space invaders",
-    "Sends the given message with a space themed effect": "Verstuur het bericht met een space-thema-effect",
+    "Sends the given message with a space themed effect": "Verstuur het bericht met een ruimte-thema-effect",
     "See when people join, leave, or are invited to your active room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd in uw actieve kamer",
     "Kick, ban, or invite people to your active room, and make you leave": "Verwijder, verban of nodig personen uit voor uw actieve kamer en uzelf laten vertrekken",
     "See when people join, leave, or are invited to this room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd voor deze kamer",
@@ -3316,29 +3316,29 @@
     "%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)s veranderden de server ACLs",
     "%(severalUsers)schanged the server ACLs %(count)s times|other": "%(severalUsers)s veranderden de server ACLs %(count)s keer",
     "Message search initialisation failed, check <a>your settings</a> for more information": "Bericht zoeken initialisatie mislukt, controleer <a>uw instellingen</a> voor meer informatie",
-    "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Stel adressen in voor deze space zodat personen deze ruimte kunnen vinden via uw homeserver (%(localDomain)s)",
+    "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Stel adressen in voor deze ruimte zodat personen deze ruimte kunnen vinden via uw server (%(localDomain)s)",
     "To publish an address, it needs to be set as a local address first.": "Om een adres te publiceren, moet het eerst als een lokaaladres worden ingesteld.",
     "Published addresses can be used by anyone on any server to join your room.": "Gepubliceerde adressen kunnen door iedereen op elke server gebruikt worden om bij uw gesprek te komen.",
-    "Published addresses can be used by anyone on any server to join your space.": "Gepubliceerde adressen kunnen door iedereen op elke server gebruikt worden om uw space te betreden.",
-    "This space has no local addresses": "Deze space heeft geen lokaaladres",
-    "Space information": "Spaceinformatie",
+    "Published addresses can be used by anyone on any server to join your space.": "Gepubliceerde adressen kunnen door iedereen op elke server gebruikt worden om uw ruimte te betreden.",
+    "This space has no local addresses": "Deze ruimte heeft geen lokaaladres",
+    "Space information": "Ruimte-informatie",
     "Collapse": "Invouwen",
     "Expand": "Uitvouwen",
-    "Recommended for public spaces.": "Aanbevolen voor openbare spaces.",
-    "Allow people to preview your space before they join.": "Personen toestaan een voorbeeld van uw space te zien voor deelname.",
-    "Preview Space": "Voorbeeld Space",
+    "Recommended for public spaces.": "Aanbevolen voor publieke ruimtes.",
+    "Allow people to preview your space before they join.": "Personen toestaan een voorvertoning van uw ruimte te zien voor deelname.",
+    "Preview Space": "Ruimte voorvertoning",
     "only invited people can view and join": "alleen uitgenodigde personen kunnen lezen en deelnemen",
     "anyone with the link can view and join": "iedereen met een link kan lezen en deelnemen",
     "Decide who can view and join %(spaceName)s.": "Bepaal wie kan lezen en deelnemen aan %(spaceName)s.",
     "Visibility": "Zichtbaarheid",
-    "This may be useful for public spaces.": "Dit kan nuttig zijn voor openbare spaces.",
-    "Guests can join a space without having an account.": "Gasten kunnen deelnemen aan een space zonder een account.",
+    "This may be useful for public spaces.": "Dit kan nuttig zijn voor publieke ruimtes.",
+    "Guests can join a space without having an account.": "Gasten kunnen deelnemen aan een ruimte zonder een account.",
     "Enable guest access": "Gastentoegang inschakelen",
-    "Failed to update the history visibility of this space": "Het bijwerken van de geschiedenis leesbaarheid voor deze space is mislukt",
-    "Failed to update the guest access of this space": "Het bijwerken van de gastentoegang van deze space is niet gelukt",
-    "Failed to update the visibility of this space": "Het bijwerken van de zichtbaarheid van deze space is mislukt",
+    "Failed to update the history visibility of this space": "Het bijwerken van de geschiedenis leesbaarheid voor deze ruimte is mislukt",
+    "Failed to update the guest access of this space": "Het bijwerken van de gastentoegang van deze ruimte is niet gelukt",
+    "Failed to update the visibility of this space": "Het bijwerken van de zichtbaarheid van deze ruimte is mislukt",
     "Address": "Adres",
-    "e.g. my-space": "v.b. mijn-space",
+    "e.g. my-space": "v.b. mijn-ruimte",
     "Silence call": "Oproep dempen",
     "Sound on": "Geluid aan",
     "Show notification badges for People in Spaces": "Toon meldingsbadge voor personen in spaces",
@@ -3369,7 +3369,7 @@
     "%(targetName)s accepted an invitation": "%(targetName)s accepteerde de uitnodiging",
     "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepteerde de uitnodiging voor %(displayName)s",
     "Some invites couldn't be sent": "Sommige uitnodigingen konden niet verstuurd worden",
-    "We sent the others, but the below people couldn't be invited to <RoomName/>": "De anderen zijn verstuurd, maar de volgende mensen konden niet worden uitgenodigd voor <RoomName/>",
+    "We sent the others, but the below people couldn't be invited to <RoomName/>": "De anderen zijn verstuurd, maar de volgende personen konden niet worden uitgenodigd voor <RoomName/>",
     "Unnamed audio": "Naamloze audio",
     "Error processing audio message": "Fout bij verwerking audiobericht",
     "Show %(count)s other previews|one": "%(count)s andere preview weergeven",
@@ -3427,21 +3427,21 @@
     "Could not connect media": "Mediaverbinding mislukt",
     "This call has ended": "Deze oproep is beëindigd",
     "Connected": "Verbonden",
-    "Spaces with access": "Spaces met toegang",
-    "Anyone in a space can find and join. <a>Edit which spaces can access here.</a>": "Iedereen in een space kan het gesprek vinden en aan deelnemen. <a>Wijzig welke spaces toegang hebben hier.</a>",
-    "Currently, %(count)s spaces have access|other": "Momenteel hebben %(count)s spaces toegang",
+    "Spaces with access": "Ruimtes met toegang",
+    "Anyone in a space can find and join. <a>Edit which spaces can access here.</a>": "Iedereen in een ruimte kan zoeken en deelnemen. <a>Wijzig hier welke ruimtes toegang hebben.</a>",
+    "Currently, %(count)s spaces have access|other": "Momenteel hebben %(count)s ruimtes toegang",
     "& %(count)s more|other": "& %(count)s meer",
     "Upgrade required": "Upgrade noodzakelijk",
     "Anyone can find and join.": "Iedereen kan hem vinden en deelnemen.",
     "Only invited people can join.": "Alleen uitgenodigde personen kunnen deelnemen.",
     "Private (invite only)": "Privé (alleen op uitnodiging)",
-    "This upgrade will allow members of selected spaces access to this room without an invite.": "Deze upgrade maakt het mogelijk voor leden van geselecteerde spaces om toegang te krijgen tot dit gesprek zonder een uitnodiging.",
-    "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Dit maakt het makkelijk om kamers privé te houden voor een space, terwijl personen in de space hem kunnen vinden en aan deelnemen. Alle nieuwe kamers in deze space hebben deze optie beschikbaar.",
-    "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Om space leden te helpen met het vinden van en deel te nemen aan privékamers, ga naar uw kamerinstellingen voor veiligheid & privacy.",
-    "Help space members find private rooms": "Help space leden privékamers te vinden",
-    "Help people in spaces to find and join private rooms": "Help personen in spaces om privékamers te vinden en aan deel te nemen",
-    "New in the Spaces beta": "Nieuw in de spaces beta",
-    "Everyone in <SpaceName/> will be able to find and join this room.": "Iedereen in <SpaceName/> kan dit gesprek vinden en aan deelnemen.",
+    "This upgrade will allow members of selected spaces access to this room without an invite.": "Deze upgrade maakt het mogelijk voor leden van geselecteerde ruimtes om toegang te krijgen tot deze kamer zonder een uitnodiging.",
+    "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Dit maakt het makkelijk om kamers privé te houden voor een ruimte, terwijl personen in de ruimte hem kunnen vinden en aan deelnemen. Alle nieuwe kamers in deze ruimte hebben deze optie beschikbaar.",
+    "To help space members find and join a private room, go to that room's Security & Privacy settings.": "Om ruimte leden te helpen met het vinden van en deel te nemen aan privékamers, ga naar uw kamerinstellingen voor veiligheid & privacy.",
+    "Help space members find private rooms": "Help ruimte leden privékamers te vinden",
+    "Help people in spaces to find and join private rooms": "Help personen in ruimtes om privékamers te vinden en aan deel te nemen",
+    "New in the Spaces beta": "Nieuw in de ruimtes beta",
+    "Everyone in <SpaceName/> will be able to find and join this room.": "Iedereen in <SpaceName/> kan deze kamer vinden en aan deelnemen.",
     "Image": "Afbeelding",
     "Sticker": "Sticker",
     "They didn't pick up": "Ze hebben niet opgenomen",
@@ -3452,26 +3452,26 @@
     "Access": "Toegang",
     "People with supported clients will be able to join the room without having a registered account.": "Personen met geschikte apps zullen aan de kamer kunnen deelnemen zonder een account te hebben.",
     "Decide who can join %(roomName)s.": "Kies wie kan deelnemen aan %(roomName)s.",
-    "Space members": "Space leden",
-    "Anyone in a space can find and join. You can select multiple spaces.": "Iedereen in een space kan zoeken en deelnemen. U kunt meerdere spaces selecteren.",
+    "Space members": "Ruimte leden",
+    "Anyone in a space can find and join. You can select multiple spaces.": "Iedereen in een ruimte kan zoeken en deelnemen. U kunt meerdere ruimtes selecteren.",
     "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Iedereen in %(spaceName)s kan zoeken en deelnemen. U kunt ook andere spaces selecteren.",
-    "Visible to space members": "Zichtbaar voor space leden",
+    "Visible to space members": "Zichtbaar voor ruimte leden",
     "Public room": "Openbaar gesprek",
     "Private room (invite only)": "Privégesprek (alleen op uitnodiging)",
     "Create a room": "Gesprek aanmaken",
     "Only people invited will be able to find and join this room.": "Alleen uitgenodigde personen kunnen dit gesprek vinden en aan deelnemen.",
-    "Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Iedereen kan dit gesprek vinden en aan deelnemen, niet alleen leden van <SpaceName/>.",
+    "Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Iedereen kan deze kamer vinden en aan deelnemen, niet alleen leden van <SpaceName/>.",
     "You can change this at any time from room settings.": "U kan dit op elk moment wijzigen vanuit de gespreksinstellingen.",
     "Error downloading audio": "Fout bij downloaden van audio",
     "<b>Please note upgrading will make a new version of the room</b>. All current messages will stay in this archived room.": "<b>Let op bijwerken maakt een nieuwe versie van dit gesprek</b>. Alle huidige berichten blijven in dit gearchiveerde gesprek.",
     "Automatically invite members from this room to the new one": "Automatisch leden uitnodigen van dit gesprek in de nieuwe",
     "These are likely ones other room admins are a part of.": "Dit zijn waarschijnlijk kamers waar andere kamerbeheerders deel van uitmaken.",
-    "Other spaces or rooms you might not know": "Andere spaces of kamers die u misschien niet kent",
-    "Spaces you know that contain this room": "Spaces die u kent met dit gesprek",
-    "Search spaces": "Spaces zoeken",
-    "Decide which spaces can access this room. If a space is selected, its members can find and join <RoomName/>.": "Kies welke spaces toegang hebben tot dit gesprek. Als een space is geselecteerd kunnen deze leden <RoomName/> vinden en aan deelnemen.",
-    "Select spaces": "Spaces selecteren",
-    "You're removing all spaces. Access will default to invite only": "U verwijderd alle spaces. De toegang zal standaard alleen op uitnodiging zijn",
+    "Other spaces or rooms you might not know": "Andere ruimtes of kamers die u misschien niet kent",
+    "Spaces you know that contain this room": "Ruimtes die u kent met deze kamer",
+    "Search spaces": "Ruimtes zoeken",
+    "Decide which spaces can access this room. If a space is selected, its members can find and join <RoomName/>.": "Kies welke ruimtes toegang hebben tot deze kamer. Als een ruimte is geselecteerd kunnen deze leden <RoomName/> vinden en aan deelnemen.",
+    "Select spaces": "Ruimte selecteren",
+    "You're removing all spaces. Access will default to invite only": "U verwijderd alle ruimtes. De toegang zal teruggezet worden naar alleen op uitnodiging",
     "Room visibility": "Gesprekszichtbaarheid",
     "Anyone will be able to find and join this room.": "Iedereen kan de kamer vinden en aan deelnemen.",
     "Share content": "Deel inhoud",
@@ -3483,36 +3483,36 @@
     "Your camera is turned off": "Uw camera staat uit",
     "%(sharerName)s is presenting": "%(sharerName)s is aan het presenteren",
     "You are presenting": "U bent aan het presenteren",
-    "Thank you for trying Spaces. Your feedback will help inform the next versions.": "Dankuwel voor het gebruiken van Spaces. Uw feedback helpt ons volgende versies te maken.",
-    "Spaces feedback": "Spaces feedback",
-    "Spaces are a new feature.": "Spaces zijn een nieuwe functie.",
+    "Thank you for trying Spaces. Your feedback will help inform the next versions.": "Dankuwel voor het gebruiken van ruimtes. Uw feedback helpt ons volgende versies te maken.",
+    "Spaces feedback": "Ruimtes feedback",
+    "Spaces are a new feature.": "Ruimtes zijn een nieuwe functie.",
     "All rooms you're in will appear in Home.": "Alle kamers waar u in bent zullen in Home verschijnen.",
     "Send pseudonymous analytics data": "Pseudonieme analytische gegevens verzenden",
     "We're working on this, but just want to let you know.": "We zijn er nog mee bezig en wilde het u even laten weten.",
-    "Search for rooms or spaces": "Zoek naar kamers of spaces",
-    "Add space": "Space toevoegen",
+    "Search for rooms or spaces": "Kamers of ruimtes zoeken",
+    "Add space": "Ruimte toevoegen",
     "Are you sure you want to leave <spaceName/>?": "Weet u zeker dat u <spaceName/> wilt verlaten?",
     "Leave %(spaceName)s": "%(spaceName)s verlaten",
     "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.": "U bent de enige beheerder van sommige kamers of spaces die u wilt verlaten. Door deze te verlaten hebben ze geen beheerder meer.",
-    "You're the only admin of this space. Leaving it will mean no one has control over it.": "U bent de enige beheerder van deze space. Door te verlaten zal niemand er meer controle over hebben.",
+    "You're the only admin of this space. Leaving it will mean no one has control over it.": "U bent de enige beheerder van deze ruimte. Door het te verlaten zal er niemand meer controle over hebben.",
     "You won't be able to rejoin unless you are re-invited.": "U kunt niet opnieuw deelnemen behalve als u opnieuw wordt uitgenodigd.",
     "Search %(spaceName)s": "Zoek %(spaceName)s",
-    "Leave specific rooms and spaces": "Verlaat specifieke kamers en spaces",
+    "Leave specific rooms and spaces": "Specifieke kamers en ruimtes verlaten",
     "Don't leave any": "Blijf in alle",
-    "Leave all rooms and spaces": "Verlaat alle kamers en spaces",
-    "Want to add an existing space instead?": "Een bestaande space toevoegen?",
-    "Private space (invite only)": "Privé space (alleen op uitnodiging)",
-    "Space visibility": "Space zichtbaarheid",
-    "Add a space to a space you manage.": "Voeg een space toe aan een space die u beheerd.",
-    "Only people invited will be able to find and join this space.": "Alleen uitgenodigde personen kunnen deze space vinden en aan deelnemen.",
-    "Anyone will be able to find and join this space, not just members of <SpaceName/>.": "Iedereen zal in staat zijn om deze space te vinden en aan deel te nemen, niet alleen leden van <SpaceName/>.",
+    "Leave all rooms and spaces": "Alle kamers en ruimtes verlaten",
+    "Want to add an existing space instead?": "Een bestaande ruimte toevoegen?",
+    "Private space (invite only)": "Privéruimte (alleen op uitnodiging)",
+    "Space visibility": "Ruimte zichtbaarheid",
+    "Add a space to a space you manage.": "Voeg een ruimte toe aan een ruimte die u beheerd.",
+    "Only people invited will be able to find and join this space.": "Alleen uitgenodigde personen kunnen deze ruimte vinden en aan deelnemen.",
+    "Anyone will be able to find and join this space, not just members of <SpaceName/>.": "Iedereen zal in staat zijn om deze ruimte te vinden en aan deel te nemen, niet alleen leden van <SpaceName/>.",
     "Anyone in <SpaceName/> will be able to find and join.": "Iedereen in <SpaceName/> zal in staat zijn om te zoeken en deel te nemen.",
-    "Adding spaces has moved.": "Spaces toevoegen is verplaatst.",
+    "Adding spaces has moved.": "Ruimtes toevoegen is verplaatst.",
     "Search for rooms": "Naar kamers zoeken",
-    "Search for spaces": "Naar spaces zoeken",
-    "Create a new space": "Maak een nieuwe space",
-    "Want to add a new space instead?": "Een nieuwe space toevoegen?",
-    "Add existing space": "Bestaande space toevoegen",
+    "Search for spaces": "Naar ruimtes zoeken",
+    "Create a new space": "Maak een nieuwe ruimte",
+    "Want to add a new space instead?": "Een nieuwe ruimte toevoegen?",
+    "Add existing space": "Bestaande ruimte toevoegen",
     "Decrypting": "Ontsleutelen",
     "Show all rooms": "Alle kamers tonen",
     "Give feedback.": "Feedback geven.",
@@ -3536,36 +3536,36 @@
     "Start the camera": "Camera starten",
     "If a community isn't shown you may not have permission to convert it.": "Als een gemeenschap niet zichtbaar is heeft u geen rechten om hem om te zetten.",
     "Show my Communities": "Mijn gemeenschappen weergeven",
-    "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.": "Gemeenschappen zijn gearchiveerd om ruimte te maken voor Spaces, maar u kunt uw gemeenschap omzetten naar een space hieronder. Hierdoor bent u er zeker van dat uw gesprekken de nieuwste functies krijgen.",
-    "Create Space": "Space aanmaken",
-    "Open Space": "Space openen",
-    "To join an existing space you'll need an invite.": "Om deel te nemen aan een bestaande space heeft u een uitnodiging nodig.",
-    "You can also create a Space from a <a>community</a>.": "U kunt ook een Space maken van een <a>gemeenschap</a>.",
+    "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.": "Gemeenschappen zijn gearchiveerd voor de nieuwe functie ruimtes, maar u kunt uw gemeenschap nog omzetten naar een ruimte hieronder. Hierdoor bent u er zeker van dat uw gesprekken de nieuwste functies krijgen.",
+    "Create Space": "Ruimte maken",
+    "Open Space": "Ruimte openen",
+    "To join an existing space you'll need an invite.": "Om deel te nemen aan een bestaande ruimte heeft u een uitnodiging nodig.",
+    "You can also create a Space from a <a>community</a>.": "U kunt ook een ruimte maken van een <a>gemeenschap</a>.",
     "You can change this later.": "U kan dit later aanpassen.",
-    "What kind of Space do you want to create?": "Wat voor soort Space wilt u maken?",
+    "What kind of Space do you want to create?": "Wat voor soort ruimte wilt u maken?",
     "Delete avatar": "Afbeelding verwijderen",
     "Don't send read receipts": "Geen leesbevestigingen versturen",
     "Created from <Community />": "Gemaakt van <Community />",
     "Communities won't receive further updates.": "Gemeenschappen zullen geen updates meer krijgen.",
-    "Spaces are a new way to make a community, with new features coming.": "Spaces zijn de nieuwe gemeenschappen, met binnenkort meer nieuwe functies.",
-    "Communities can now be made into Spaces": "Gemeenschappen kunnen nu omgezet worden in Spaces",
-    "Ask the <a>admins</a> of this community to make it into a Space and keep a look out for the invite.": "Vraag een <a>beheerder</a> van deze gemeenschap om hem om te zetten in een Space en kijk uit naar de uitnodiging.",
-    "You can create a Space from this community <a>here</a>.": "U kunt <a>hier</a> een Space maken van uw gemeenschap.",
-    "This description will be shown to people when they view your space": "Deze omschrijving zal getoond worden aan personen die uw space bekijken",
-    "Flair won't be available in Spaces for the foreseeable future.": "Badges zijn niet beschikbaar in Spaces in de nabije toekomst.",
+    "Spaces are a new way to make a community, with new features coming.": "Ruimtes zijn de nieuwe gemeenschappen, met binnenkort meer nieuwe functies.",
+    "Communities can now be made into Spaces": "Gemeenschappen kunnen nu omgezet worden in ruimtes",
+    "Ask the <a>admins</a> of this community to make it into a Space and keep a look out for the invite.": "Vraag een <a>beheerder</a> van deze gemeenschap om hem om te zetten in een ruimte en kijk uit naar de uitnodiging.",
+    "You can create a Space from this community <a>here</a>.": "U kunt <a>hier</a> een ruimte maken van uw gemeenschap.",
+    "This description will be shown to people when they view your space": "Deze omschrijving zal getoond worden aan personen die uw ruimte bekijken",
+    "Flair won't be available in Spaces for the foreseeable future.": "Badges zijn niet beschikbaar in ruimtes in de nabije toekomst.",
     "All rooms will be added and all community members will be invited.": "Alle kamers zullen worden toegevoegd en alle gemeenschapsleden zullen worden uitgenodigd.",
-    "A link to the Space will be put in your community description.": "Een link naar deze Space zal geplaatst worden in de gemeenschapsomschrijving.",
-    "Create Space from community": "Space van gemeenschap maken",
+    "A link to the Space will be put in your community description.": "In de gemeenschapsomschrijving zal een link naar deze ruimte worden geplaatst.",
+    "Create Space from community": "Ruimte van gemeenschap maken",
     "Failed to migrate community": "Omzetten van de gemeenschap is mislukt",
     "To create a Space from another community, just pick the community in Preferences.": "Om een Space te maken van een gemeenschap kiest u de gemeenschap in Instellingen.",
     "<SpaceName/> has been made and everyone who was a part of the community has been invited to it.": "<SpaceName/> is gemaakt en iedereen die lid was van de gemeenschap is ervoor uitgenodigd.",
-    "Space created": "Space aangemaakt",
-    "To view Spaces, hide communities in <a>Preferences</a>": "Om Spaces te zien, verberg gemeenschappen in uw <a>Instellingen</a>",
-    "This community has been upgraded into a Space": "Deze gemeenschap is geupgrade naar een Space",
+    "Space created": "Ruimte aangemaakt",
+    "To view Spaces, hide communities in <a>Preferences</a>": "Om ruimtes te zien, verberg gemeenschappen in uw <a>Instellingen</a>",
+    "This community has been upgraded into a Space": "Deze gemeenschap is geupgrade naar een ruimte",
     "Unknown failure: %(reason)s": "Onbekende fout: %(reason)s",
     "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Debug logs bevatten applicatie gebruiksgegevens inclusief uw inlognaam, de ID's of aliassen van de kamers of groepen die u hebt bezocht, welke UI elementen u het laatst hebt gebruikt, en de inlognamen van andere personen. Ze bevatten geen berichten.",
     "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Als u een bug hebt ingediend via GitHub, kunnen debug logs ons helpen het probleem op te sporen. Debug logs bevatten applicatie gebruiksgegevens inclusief uw inlognaam, de ID's of aliassen van de kamers of groepen die u hebt bezocht, welke UI elementen u het laatst hebt gebruikt, en de inlognamen van andere personen. Ze bevatten geen berichten.",
-    "Rooms and spaces": "Kamers en spaces",
+    "Rooms and spaces": "Kamers en ruimtes",
     "Results": "Resultaten",
     "Enable encryption in settings.": "Versleuteling inschakelen in instellingen.",
     "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.": "Uw privéberichten zijn versleuteld, maar deze kamer niet. Dit komt vaak doordat u een niet ondersteund apparaat of methode, zoals e-mailuitnodigingen.",
@@ -3590,5 +3590,37 @@
     "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s prikte een bericht vast aan deze kamer. Bekijk alle vastgeprikte berichten.",
     "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s prikte <a>een bericht</a> aan deze kamer. Bekijk alle <b>vastgeprikte berichten</b>.",
     "Currently, %(count)s spaces have access|one": "Momenteel heeft één ruimte toegang",
-    "& %(count)s more|one": "& %(count)s meer"
+    "& %(count)s more|one": "& %(count)s meer",
+    "Some encryption parameters have been changed.": "Enkele versleutingsparameters zijn gewijzigd.",
+    "Role in <RoomName/>": "Rol in <RoomName/>",
+    "Explore %(spaceName)s": "%(spaceName)s ontdekken",
+    "Send a sticker": "Verstuur een sticker",
+    "Reply to thread…": "Reageer op draad…",
+    "Reply to encrypted thread…": "Reageer op versleutelde draad…",
+    "Add emoji": "Emoji toevoegen",
+    "Unknown failure": "Onbekende fout",
+    "Failed to update the join rules": "Het updaten van de deelname regels is mislukt",
+    "Select the roles required to change various parts of the space": "Selecteer de rollen die vereist zijn om onderdelen van de ruimte te wijzigen",
+    "Change description": "Omschrijving wijzigen",
+    "Change main address for the space": "Hoofdadres van ruimte wijzigen",
+    "Change space name": "Ruimtenaam wijzigen",
+    "Change space avatar": "Ruimte-afbeelding wijzigen",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Iedereen in <spaceName/> kan zoeken en deelnemen. U kunt ook andere ruimtes selecteren.",
+    "Message didn't send. Click for info.": "Bericht is niet verstuur. Klik voor meer info.",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "Om aan %(communityName)s deel te nemen, wissel naar gemeenschappen in uw <a>instellingen</a>",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "Om %(communityName)s te bekijken, wissel naar gemeenschappen in uw <a>instellingen</a>",
+    "Private community": "Privégemeenschap",
+    "Public community": "Publieke gemeenschap",
+    "Message": "Bericht",
+    "Upgrade anyway": "Upgrade alsnog uitvoeren",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Deze kamer is in ruimtes waar u geen beheerder van bent. In deze ruimtes zal de oude kamer nog worden getoond, maar leden zullen een melding krijgen om deel te nemen aan de nieuwe kamer.",
+    "Before you upgrade": "Voordat u upgrade",
+    "To join a space you'll need an invite.": "Om te kunnen deelnemen aan een ruimte heeft u een uitnodiging nodig.",
+    "You can also make Spaces from <a>communities</a>.": "U kunt ook ruimtes maken van uw <a>gemeenschappen</a>.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Tijdelijk gemeenschappen tonen in plaats van ruimtes voor deze sessie. Ondersteuning zal worden verwijderd in de nabije toekomst. Dit zal Element herladen.",
+    "Display Communities instead of Spaces": "Gemeenschappen tonen ipv ruimtes",
+    "Joining space …": "Deelnemen aan ruimte…",
+    "To join this Space, hide communities in your <a>preferences</a>": "Om deel te nemen aan de ruimte, verberg gemeenschappen in uw <a>instellingen</a>",
+    "To view this Space, hide communities in your <a>preferences</a>": "Om deze ruimte te bekijken, verberg gemeenschappen in uw <a>instellingen</a>",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s reageerde met %(content)s"
 }
diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json
index d5de4d2aac..235b8a58f8 100644
--- a/src/i18n/strings/pt_BR.json
+++ b/src/i18n/strings/pt_BR.json
@@ -3244,5 +3244,161 @@
     "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s removeu seu nome de exibição (%(oldDisplayName)s)",
     "%(senderName)s set their display name to %(displayName)s": "%(senderName)s definiu seu nome de exibição para %(displayName)s",
     "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s mudou seu nome de exibição para %(displayName)s",
-    "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Você pode sair do beta a qualquer momento nas configurações ou tocando em um emblema beta, como o mostrado acima."
+    "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Você pode sair do beta a qualquer momento nas configurações ou tocando em um emblema beta, como o mostrado acima.",
+    "If a community isn't shown you may not have permission to convert it.": "Se uma comunidade não é exibida você pode não ter a permissão para convertê-la.",
+    "Show my Communities": "Mostre minhas Comunidades",
+    "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.": "Comunidades foram arquivadas para abrir caminho para os Espaços, mas você pode converter suas comunidades em Espaços logo abaixo. Convertê-las garantirá que suas conversas tenhas as novidades mais recentes.",
+    "Create Space": "Criar um Espaço",
+    "Open Space": "Espaço Aberto",
+    "Your access token gives full access to your account. Do not share it with anyone.": "Seu token de acesso dá acesso total à sua conta. Não o compartilhe com ninguém.",
+    "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, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Se você informou um bug através do GitHub, os relatórios de erros podem nos ajudar a encontrar o problema. Os relatórios de erros contêm dados de uso do aplicativo, incluindo seu nome de usuário, os IDs ou apelidos das salas ou comunidades que você visitou e os nomes de usuários de seus contatos. Eles não contêm suas mensagens.",
+    "Olm version:": "Versão do Olm:",
+    "There was an error loading your notification settings.": "Um erro ocorreu ao carregar suas configurações de notificação.",
+    "Enable email notifications for %(email)s": "Habilita notificação por emails para %(email)s",
+    "An error occurred whilst saving your notification preferences.": "Um erro ocorreu enquanto suas preferências de notificação eram salvas.",
+    "Upgrade anyway": "Faça o upgrade mesmo assim",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Esta sala está em alguns espaços que você não é um administrador. Nestes espaços, a sala antiga ainda será exibida, mas as pessoas receberão um prompt para se juntarem a sala nova.",
+    "Anyone in a space can find and join. You can select multiple spaces.": "Qualquer um em um espaço pode encontrar e se juntar. Você pode selecionar múltiplos espaços.",
+    "Message search initialisation failed": "Falha na inicialização da pesquisa de mensagens",
+    "Allow people to preview your space before they join.": "Permite que pessoas vejam seu espaço antes de entrarem.",
+    "Failed to update the visibility of this space": "Falha ao atualizar a visibilidade deste espaço",
+    "Decide who can view and join %(spaceName)s.": "Decide quem pode ver e se juntar a %(spaceName)s.",
+    "Guests can join a space without having an account.": "Convidados podem se juntar a um espaço sem ter uma conta.",
+    "Failed to update the history visibility of this space": "Falha ao atualizar a visibilidade do histórico deste espaço",
+    "Failed to update the guest access of this space": "Falha ao atualizar o acesso de convidados a este espaço",
+    "Add some details to help people recognise it.": "Adicione alguns detalhes para ajudar as pessoas a reconhecê-lo.",
+    "To join a space you'll need an invite.": "Para se juntar a um espaço você precisará de um convite.",
+    "You can also make Spaces from <a>communities</a>.": "Você também pode criar Espaços a partir de <a>comunidades</a>.",
+    "Invite only, best for yourself or teams": "Somente convite, melhor para si mesmo(a) ou para equipes",
+    "You can change this later.": "Você pode mudar isso depois.",
+    "What kind of Space do you want to create?": "Que tipo de espaço você deseja criar?",
+    "Delete avatar": "Remover foto de perfil",
+    "Mute the microphone": "Silenciar o microfone",
+    "Unmute the microphone": "Desmutar o microfone",
+    "Dialpad": "Teclado de discagem",
+    "More": "Mais",
+    "Show sidebar": "Exibir a barra lateral",
+    "Hide sidebar": "Esconder a barra lateral",
+    "Start sharing your screen": "Começar a compartilhar sua tela",
+    "Stop sharing your screen": "Parar de compartilhar sua tela",
+    "Stop the camera": "Desligar a câmera",
+    "Start the camera": "Ativar a câmera",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Temporariamente exibe comunidades ao invés de Espaços nesta sessão. Suporte para isto será removido no futuro próximo. Isto recarregará o Element.",
+    "Display Communities instead of Spaces": "Exibe Comunidades ao invés de Espaços",
+    "Low bandwidth mode (requires compatible homeserver)": "Modo de internet lenta (requer um servidor compatível)",
+    "Autoplay videos": "Reproduzir vídeos automaticamente",
+    "Autoplay GIFs": "Reproduzir GIFs automaticamente",
+    "Don't send read receipts": "Não enviar confirmações de leitura",
+    "New layout switcher (with message bubbles)": "Novo gerenciador de layouts (com mensagens em bolhas)",
+    "Multiple integration managers (requires manual setup)": "Múltiplos gerenciadores de integração (requer configuração manual)",
+    "Threaded messaging": "Mensagens em fios",
+    "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Protótipo de reportar para os moderadores. Em salas que tem suporte a moderação, o botão `reportar` lhe permitirá reportar abuso para os moderadores da sala",
+    "This makes it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "Isto facilita com que salas se mantenham privadas em um espaço, enquanto permitem que pessoas que se juntem ao espaço as encontrem e entrem. Todas as salas novas em um espaço terão esta opção disponível.",
+    "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s fixou uma mensagem nesta sala. Veja todas as mensagens fixadas.",
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fixou <a>uma mensagens</a> nesta sala. Veja todas as <b>mensagens fixadas</b>.",
+    "You may contact me if you have any follow up questions": "Vocês podem me contactar se tiverem quaisquer perguntas subsequentes",
+    "Thank you for your feedback, we really appreciate it.": "Agradecemos pelo seu feedback, realmente apreciamos isso.",
+    "Search for rooms or people": "Procurar por salas ou pessoas",
+    "Forward message": "Encaminhar",
+    "Open link": "Abrir link",
+    "Sent": "Enviado",
+    "Sending": "Enviando",
+    "You don't have permission to do this": "Você não tem permissão para fazer isso",
+    "Adding...": "Adicionando…",
+    "Add a space to a space you manage.": "Adicionar um espaço à um espaço que você gerencia.",
+    "Only people invited will be able to find and join this space.": "Apenas convidados poderão encontrar e entrar neste espaço.",
+    "Anyone will be able to find and join this space, not just members of <SpaceName/>.": "Qualquer um poderá encontrar e entrar neste espaço, não somente membros de <SpaceName/>.",
+    "Anyone in <SpaceName/> will be able to find and join.": "Todos em <SpaceName/> poderão ver e entrar.",
+    "Public space": "Espaço público",
+    "Private space (invite only)": "Espaço privado (apenas com convite)",
+    "Space visibility": "Visibilidade do Espaço",
+    "This description will be shown to people when they view your space": "Esta descrição será exibida para as pessoas quando elas virem seu espaço",
+    "All rooms will be added and all community members will be invited.": "Todas as salas serão adicionadas e todos os membros da comunidade serão convidados.",
+    "A link to the Space will be put in your community description.": "Um link para o Espaço será colocado na descrição da sua comunidade.",
+    "Create Space from community": "Criar Espaço a partir da comunidade",
+    "Failed to migrate community": "Falha ao migrar a comunidade",
+    "To create a Space from another community, just pick the community in Preferences.": "Para criar um Espaço a partir de outra comunidade, apenas escolha a comunidade nas Preferências.",
+    "<SpaceName/> has been made and everyone who was a part of the community has been invited to it.": "<SpaceName/> foi criado e todos que eram parte da comunidade foram convidados.",
+    "Space created": "Espaço criado",
+    "To view Spaces, hide communities in <a>Preferences</a>": "Para ver os Espaços, esconda as comunidades em <a>Preferências</a>",
+    "Visible to space members": "Visível para membros do espaço",
+    "Public room": "Sala pública",
+    "Private room (invite only)": "Sala privada (apenas com convite)",
+    "Room visibility": "Visibilidade da sala",
+    "Create a room": "Criar uma sala",
+    "Only people invited will be able to find and join this room.": "Apenas convidados poderão encontrar e entrar nesta sala.",
+    "Anyone will be able to find and join this room.": "Qualquer um poderá encontrar e entrar nesta sala.",
+    "Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Qualquer um poderá encontrar e entrar nesta sala, não somente membros de <SpaceName/>.",
+    "You can change this at any time from room settings.": "Você pode mudar isto em qualquer momento nas configurações da sala.",
+    "Everyone in <SpaceName/> will be able to find and join this room.": "Todos em <SpaceName/> podersão ver e entrar nesta sala.",
+    "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Os relatórios de erros contêm dados de uso do aplicativo, incluindo seu nome de usuário, os IDs ou nomes das salas ou comunidades que você visitou e os nomes de usuários de seus contatos. Eles não contêm suas mensagens.",
+    "To leave the beta, visit your settings.": "Para sair do beta, vá nas suas configurações.",
+    "Search for rooms": "Buscar salas",
+    "Add existing rooms": "Adicionar salas existentes",
+    "Adding rooms... (%(progress)s out of %(count)s)|one": "Adicionando sala…",
+    "Adding rooms... (%(progress)s out of %(count)s)|other": "Adicionando salas… (%(progress)s de %(count)s)",
+    "Create a new space": "Criar um novo espaço",
+    "Add existing space": "Adicionar espaço existente",
+    "You are not allowed to view this server's rooms list": "Você não tem a permissão para ver a lista de salas deste servidor",
+    "Please provide an address": "Por favor, digite um endereço",
+    "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)s mudou as <a>mensagens fixadas</a> para a sala %(count)s vezes.",
+    "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)smudaram as <a>mensagens fixadas</a> para a sala %(count)s vezes.",
+    "%(count)s people you know have already joined|one": "%(count)s pessoa que você conhece já entrou",
+    "%(count)s people you know have already joined|other": "%(count)s pessoas que você conhece já entraram",
+    "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s",
+    "Including %(commaSeparatedMembers)s": "Incluindo %(commaSeparatedMembers)s",
+    "View all %(count)s members|one": "Ver 1 membro",
+    "View all %(count)s members|other": "Ver todos os %(count)s membros",
+    "Share content": "Compatilhe conteúdo",
+    "Application window": "Janela da aplicação",
+    "Share entire screen": "Compartilhe a tela inteira",
+    "Message search initialisation failed, check <a>your settings</a> for more information": "Falha na inicialização da pesquisa por mensagem, confire <a>suas configurações</a> para mais informações",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s reagiram com %(content)s",
+    "Add reaction": "Adicionar reação",
+    "Error processing voice message": "Erro ao processar a mensagem de voz",
+    "Image": "Imagem",
+    "Sticker": "Figurinha",
+    "Error processing audio message": "Erro ao processar a mensagem de áudio",
+    "Some encryption parameters have been changed.": "Alguns parâmetros de criptografia foram modificados.",
+    "Decrypting": "Decriptando",
+    "The call is in an unknown state!": "A chamada está em um estado desconhecido!",
+    "Missed call": "Chamada perdida",
+    "Unknown failure: %(reason)s": "Falha desconhecida: %(reason)s",
+    "An unknown error occurred": "Um erro desconhecido ocorreu",
+    "Their device couldn't start the camera or microphone": "Este dispositivo não conseguiu iniciar a câmera ou microfone",
+    "Connection failed": "Falha na conexão",
+    "Could not connect media": "Não foi possível conectar-se à mídia",
+    "No answer": "Sem resposta",
+    "Call back": "Chamar de volta",
+    "Call declined": "Chamada recusada",
+    "Edit devices": "Editar dispositivos",
+    "Role in <RoomName/>": "Cargo em <RoomName/>",
+    "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "Você não poderá desfazer esta mudança, já que está tirando seu próprio cargo, e se você for o último usuário com privilégios neste espaço será impossível ganhá-los novamente.",
+    "Message": "Mensagem",
+    "Pinned messages": "Mensagens fixadas",
+    "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "Caso você tenha a permissão para isso, abra o menu em qualquer mensagem e selecione <b>Fixar</b> para fixá-la aqui.",
+    "Nothing pinned, yet": "Nada fixado ainda",
+    "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Defina endereços para este espaço para que usuários possam encontrá-lo através do seu servidor local (%(localDomain)s)",
+    "To publish an address, it needs to be set as a local address first.": "Para publicar um endereço, ele precisa ser configurado como um endereço local primeiro.",
+    "Published addresses can be used by anyone on any server to join your room.": "Endereços publicados podem ser usados por qualquer um em qualquer servidor para entrar em sua sala.",
+    "Published addresses can be used by anyone on any server to join your space.": "Endereços publicados podem ser usados por qualquer um em qualquer servidor para entrar em seu espaço.",
+    "Stop recording": "Parar a gravação",
+    "We didn't find a microphone on your device. Please check your settings and try again.": "Não foi possível encontrar um microfone em seu dispositivo. Confira suas configurações e tente novamente.",
+    "We were unable to access your microphone. Please check your browser settings and try again.": "Não foi possível acessar seu microfone. Por favor, confira as configurações do seu navegador e tente novamente.",
+    "Joining space …": "Entrando no espaço …",
+    "%(count)s results in all spaces|one": "%(count)s resultado em todos os espaços",
+    "%(count)s results in all spaces|other": "%(count)s resultados em todos os espaços",
+    "Explore %(spaceName)s": "Explore %(spaceName)s",
+    "You can now share your screen by pressing the \"screen share\" button during a call. You can even do this in audio calls if both sides support it!": "Você agora pode compartilhar sua tela pressionando o botão \"compartilhar a tela\" durante uma chamada. Você pode fazer isso até mesmo em chamadas de áudio se ambos os lados tiverem suporte para isto!",
+    "Message didn't send. Click for info.": "A mensagem não foi enviada. Clique para mais informações.",
+    "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.": "Suas mensagens privadas normalmente são criptografadas, mas esta sala não é. Isto acontece normalmente por conta de um dispositivo ou método usado sem suporte, como convites via email, por exemplo.",
+    "Send voice message": "Enviar uma mensagem de voz",
+    "Send a sticker": "Enviar uma figurinha",
+    "To avoid these issues, create a <a>new public room</a> for the conversation you plan to have.": "Para evitar esses problemas, crie uma <a>nova sala pública</a> para a conversa que você planeja ter.",
+    "Are you sure you want to make this encrypted room public?": "Tem certeza que fazer com que esta sala criptografada seja pública?",
+    "Unknown failure": "Falha desconhecida",
+    "To avoid these issues, create a <a>new encrypted room</a> for the conversation you plan to have.": "Para evitar esses problemas, crie uma <a>nova sala criptografada</a> para a conversa que você planeja ter.",
+    "Are you sure you want to add encryption to this public room?": "Tem certeza que deseja adicionar criptografia para esta sala pública?",
+    "Select the roles required to change various parts of the space": "Selecionar os cargos necessários para alterar certas partes do espaço",
+    "Change main address for the space": "Mudar o endereço principal para o espaço"
 }
diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json
index 6d6bf86559..2b1d59c053 100644
--- a/src/i18n/strings/ru.json
+++ b/src/i18n/strings/ru.json
@@ -3581,5 +3581,71 @@
     "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)s изменил(а) <a>прикреплённые сообщения</a> в комнате %(count)s раз.",
     "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)s изменили <a>прикреплённые сообщения</a> в комнате %(count)s раз.",
     "Delete avatar": "Удалить аватар",
-    "Don't send read receipts": "Не отправлять уведомления о прочтении"
+    "Don't send read receipts": "Не отправлять уведомления о прочтении",
+    "Created from <Community />": "Создано из <Community />",
+    "Rooms and spaces": "Комнаты и пространства",
+    "Results": "Результаты",
+    "Communities won't receive further updates.": "Сообщества не будут получать дальнейших обновлений.",
+    "Spaces are a new way to make a community, with new features coming.": "Пространства - это новый способ создания сообщества с новыми возможностями.",
+    "Communities can now be made into Spaces": "Сообщества теперь можно преобразовать в пространства",
+    "Ask the <a>admins</a> of this community to make it into a Space and keep a look out for the invite.": "Попросите <a>администраторов</a> этого сообщества сделать его пространством и следите за приглашением в него.",
+    "You can create a Space from this community <a>here</a>.": "Вы можете создать пространство из этого сообщества <a>здесь</a>.",
+    "This description will be shown to people when they view your space": "Это описание будет показано людям, когда они будут просматривать ваше пространство",
+    "Flair won't be available in Spaces for the foreseeable future.": "В ближайшем будущем значки не будут доступны в пространствах.",
+    "All rooms will be added and all community members will be invited.": "Будут добавлены все комнаты и приглашены все участники сообщества.",
+    "A link to the Space will be put in your community description.": "Ссылка на пространство будет размещена в описании вашего сообщества.",
+    "Create Space from community": "Создать пространство из сообщества",
+    "Failed to migrate community": "Не удалось преобразовать сообщество",
+    "To create a Space from another community, just pick the community in Preferences.": "Чтобы создать пространство из другого сообщества, просто выберите сообщество в настройках.",
+    "<SpaceName/> has been made and everyone who was a part of the community has been invited to it.": "<SpaceName/> было создано, и все, кто был частью сообщества, были приглашены в него.",
+    "Space created": "Пространство создано",
+    "To view Spaces, hide communities in <a>Preferences</a>": "Чтобы просмотреть Пространства, скройте сообщества в <a>Параметрах</a>",
+    "This community has been upgraded into a Space": "Это сообщество было обновлено в пространство",
+    "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Журналы отладки содержат данные об использовании приложения, включая ваше имя пользователя, идентификаторы или псевдонимы комнат или групп, которые вы посетили, с какими элементами пользовательского интерфейса вы взаимодействовали в последний раз, а также имена пользователей других пользователей. Они не содержат сообщений.",
+    "Some encryption parameters have been changed.": "Некоторые параметры шифрования были изменены.",
+    "Unknown failure: %(reason)s": "Неизвестная ошибка: %(reason)s",
+    "No answer": "Нет ответа",
+    "Role in <RoomName/>": "Роль в <RoomName/>",
+    "Explore %(spaceName)s": "Исследовать %(spaceName)s",
+    "Enable encryption in settings.": "Включите шифрование в настройках.",
+    "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.": "Ваши личные сообщения обычно шифруются, но эта комната не шифруется. Обычно это связано с использованием неподдерживаемого устройства или метода, например, приглашения по электронной почте.",
+    "Send a sticker": "Отправить стикер",
+    "Add emoji": "Добавить смайлик",
+    "To avoid these issues, create a <a>new public room</a> for the conversation you plan to have.": "Чтобы избежать этих проблем, создайте <a>новую публичную комнату</a> для разговора, который вы планируете провести.",
+    "<b>It's not recommended to make encrypted rooms public.</b> It will mean anyone can find and join the room, so anyone can read messages. You'll get none of the benefits of encryption. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Не рекомендуется делать зашифрованные комнаты публичными.</b> Это означает, что любой может найти и присоединиться к комнате, а значит, любой может читать сообщения. Вы не получите ни одного из преимуществ шифрования. Шифрование сообщений в публичной комнате сделает получение и отправку сообщений более медленной.",
+    "Are you sure you want to make this encrypted room public?": "Вы уверены, что хотите сделать эту зашифрованную комнату публичной?",
+    "Unknown failure": "Неизвестная ошибка",
+    "Failed to update the join rules": "Не удалось обновить правила присоединения",
+    "To avoid these issues, create a <a>new encrypted room</a> for the conversation you plan to have.": "Чтобы избежать этих проблем, создайте <a>новую зашифрованную комнату</a> для разговора, который вы планируете провести.",
+    "<b>It's not recommended to add encryption to public rooms.</b>Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "<b>Не рекомендуется добавлять шифрование в публичные комнаты.</b> Любой может найти и присоединиться к публичным комнатам, поэтому любой может прочитать сообщения в них. Вы не получите ни одного из преимуществ шифрования, и вы не сможете отключить его позже. Шифрование сообщений в публичной комнате замедляет получение и отправку сообщений.",
+    "Are you sure you want to add encryption to this public room?": "Вы уверены, что хотите добавить шифрование в эту публичную комнату?",
+    "Select the roles required to change various parts of the space": "Выберите роли, необходимые для изменения различных частей пространства",
+    "Change description": "Изменить описание",
+    "Change main address for the space": "Изменить основной адрес для пространства",
+    "Change space name": "Изменить название пространства",
+    "Change space avatar": "Изменить аватар пространства",
+    "If a community isn't shown you may not have permission to convert it.": "Если сообщество не показано, у вас может не быть разрешения на его преобразование.",
+    "Show my Communities": "Показать мои сообщества",
+    "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.": "Сообщества были архивированы, чтобы освободить место для пространств, но вы можете преобразовать свои сообщества в пространства ниже. Преобразование позволит вашим беседам получить новейшие функции.",
+    "Create Space": "Создать пространство",
+    "Open Space": "Открыть пространство",
+    "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, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Если вы отправили ошибку через GitHub, журналы отладки могут помочь нам отследить проблему. Журналы отладки содержат данные об использовании приложения, включая ваше имя пользователя, идентификаторы или псевдонимы комнат или групп, которые вы посетили, с какими элементами пользовательского интерфейса вы взаимодействовали в последний раз, а также имена других пользователей. Они не содержат сообщений.",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Любой человек в <spaceName/> может найти и присоединиться. Вы можете выбрать и другие пространства.",
+    "Currently, %(count)s spaces have access|one": "В настоящее время пространство имеет доступ",
+    "& %(count)s more|one": "и %(count)s еще",
+    "Cross-signing is ready but keys are not backed up.": "Кросс-подпись готова, но ключи не резервируются.",
+    "To join an existing space you'll need an invite.": "Чтобы присоединиться к существующему пространству, вам потребуется приглашение.",
+    "You can also create a Space from a <a>community</a>.": "Вы также можете создать пространство из <a>сообщества</a>.",
+    "You can change this later.": "Вы можете изменить это позже.",
+    "What kind of Space do you want to create?": "Какое пространство вы хотите создать?",
+    "Low bandwidth mode (requires compatible homeserver)": "Режим низкой пропускной способности (требуется совместимый домашний сервер)",
+    "Autoplay videos": "Автовоспроизведение видео",
+    "Autoplay GIFs": "Автовоспроизведение GIF",
+    "Multiple integration managers (requires manual setup)": "Несколько менеджеров интеграции (требуется ручная настройка)",
+    "The above, but in <Room /> as well": "Вышеописанное, но также в <Room />",
+    "The above, but in any room you are joined or invited to as well": "Вышеперечисленное, но также в любой комнате, в которую вы вошли или приглашены",
+    "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s открепляет сообщение из этой комнаты. Просмотрите все прикрепленые сообщения.",
+    "%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s открепляет <a>сообщение</a> из этой комнаты. Просмотрите все <b>прикрепленые сообщения</b>.",
+    "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s прикрепляет сообщение в этой комнате. Просмотрите все прикрепленные сообщения.",
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s прикрепляет <a>сообщение</a> в этой комнате. Просмотрите все <b>прикрепленые сообщения</b>."
 }
diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json
index 937461dbc7..386963ea29 100644
--- a/src/i18n/strings/sq.json
+++ b/src/i18n/strings/sq.json
@@ -3690,5 +3690,39 @@
     "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s hoqi fiksimin e një mesazhi nga kjo dhomë. Shihni krejt mesazhet e fiksuar.",
     "%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s hoqi fiksimin e <a>një mesazhi</a> nga kjo dhomë. Shihni krejt <b>mesazhet e fiksuar</b>.",
     "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s fiksoi një mesazh te kjo dhomë. Shihni krejt mesazhet e fiksuar.",
-    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fiksoi <a>një mesazh</a> te kjo dhomë. Shini krejt <b>mesazhet e fiksuar</b>."
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fiksoi <a>një mesazh</a> te kjo dhomë. Shini krejt <b>mesazhet e fiksuar</b>.",
+    "Some encryption parameters have been changed.": "Janë ndryshuar disa parametra fshehtëzimi.",
+    "Role in <RoomName/>": "Rol në <RoomName/>",
+    "Currently, %(count)s spaces have access|one": "Aktualisht një hapësirë ka hyrje",
+    "& %(count)s more|one": "& %(count)s më tepër",
+    "Explore %(spaceName)s": "Eksploroni %(spaceName)s",
+    "Send a sticker": "Dërgoni një ngjitës",
+    "Reply to thread…": "Përgjigjuni te rrjedhë…",
+    "Reply to encrypted thread…": "Përgjigjuni te rrjedhë e fshehtëzuar…",
+    "Add emoji": "Shtoni emoji",
+    "Unknown failure": "Dështim i panjohur",
+    "Failed to update the join rules": "S’u arrit të përditësohen rregulla hyrjeje",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Cilido te <spaceName/> mund ta gjejë dhe hyjë në të. Mund të përzgjidhni gjithashtu hapësira të tjera.",
+    "Select the roles required to change various parts of the space": "Përzgjidhni rolet e domosdoshëm për të ndryshuar pjesë të ndryshme të hapësirës",
+    "Change description": "Ndryshoni përshkrimin",
+    "Change main address for the space": "Ndryshoni adresë kryesore për hapësirën",
+    "Change space name": "Ndryshoni emër hapësire",
+    "Change space avatar": "Ndryshoni avatar hapësire",
+    "To join this Space, hide communities in your <a>preferences</a>": "Që të hyni te kjo Hapësirë, fshihni bashkësitë te <a>parapëlqimet</a> tuaja",
+    "To view this Space, hide communities in your <a>preferences</a>": "Që të shihni këtë Hapësirë, fshihni bashkësitë te <a>parapëlqimet</a> tuaja",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "Që të hyni te %(communityName)s, kaloni te bashkësitë, që nga <a>parapëlqimet</a> tuaja",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "Që të shihni %(communityName)s, kaloni te bashkësitë, që nga <a>parapëlqimet</a> tuaja",
+    "Private community": "Bashkësi private",
+    "Public community": "Bashkësi publike",
+    "Message": "Mesazh",
+    "Message didn't send. Click for info.": "Mesazhi s’u dërgua. Klikoni për hollësi.",
+    "Upgrade anyway": "Përmirësoje, sido qoftë",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Kjo dhomë gjendet në disa hapësira për të cilat nuk jeni një nga përgjegjësit. Në këto hapësira, dhoma e vjetër prapë do të shfaqet, por njerëzve do t’u kërkohet të marrin pjesë te e reja.",
+    "Before you upgrade": "Para se të përmirësoni",
+    "To join a space you'll need an invite.": "Që të hyni në një hapësirë, do t’ju duhet një ftesë.",
+    "You can also make Spaces from <a>communities</a>.": "Mundeni edhe të krijoni Hapësira që nga <a>bashkësitë</a>.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Për këtë sesion shfaq përkohësisht bashkësi, në vend se Hapësira. Mbulimi i kësaj do të hiqet në të ardhmen e afërt. Kjo do të sjellë ringarkim të Element-it.",
+    "Display Communities instead of Spaces": "Shfaq Bashkësi, në vend se Hapësira",
+    "Joining space …": "Po hyhet në hapësirë…",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s reagoi me %(content)s"
 }
diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json
index fdd5ab36ba..1d2d39ac0d 100644
--- a/src/i18n/strings/sv.json
+++ b/src/i18n/strings/sv.json
@@ -3619,5 +3619,39 @@
     "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s avfäste ett meddelande i det här rummet. Se alla fästa meddelanden.",
     "%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s avfäste <a>ett meddelande</a> i det här rummet. Se alla <b>fästa meddelanden</b>.",
     "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s fäste ett meddelande i det här rummet. Se alla fästa meddelanden.",
-    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fäste <a>ett meddelande</a> i det här rummet. Se alla <b>fästa meddelanden</b>."
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s fäste <a>ett meddelande</a> i det här rummet. Se alla <b>fästa meddelanden</b>.",
+    "& %(count)s more|one": "& %(count)s till",
+    "Some encryption parameters have been changed.": "Vissa krypteringsparametrar har ändrats.",
+    "Role in <RoomName/>": "Roll i <RoomName/>",
+    "Explore %(spaceName)s": "Utforska %(spaceName)s",
+    "Send a sticker": "Skicka en dekal",
+    "Reply to thread…": "Svara på tråd…",
+    "Reply to encrypted thread…": "Svara på krypterad tråd…",
+    "Add emoji": "Lägg till emoji",
+    "Unknown failure": "Okänt fel",
+    "Failed to update the join rules": "Misslyckades att uppdatera regler för att gå med",
+    "Select the roles required to change various parts of the space": "Välj de roller som krävs för att ändra olika delar av utrymmet",
+    "Change description": "Ändra beskrivningen",
+    "Change main address for the space": "Byt huvudadress för utrymmet",
+    "Change space name": "Byt utrymmesnamn",
+    "Change space avatar": "Byt utrymmesavatar",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Vem som helst i <spaceName/> kan hitta och gå med. Du kan välja andra utrymmen också.",
+    "Currently, %(count)s spaces have access|one": "Just nu har ett utrymme åtkomst",
+    "To join this Space, hide communities in your <a>preferences</a>": "För att gå med i det här utrymmet, dölj gemenskaper i dina <a>inställningar</a>",
+    "To view this Space, hide communities in your <a>preferences</a>": "För att se det här utrymmet, dölj gemenskaper i dina <a>inställningar</a>",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "För att gå med i %(communityName)s, byt till gemenskaper i dina <a>inställningar</a>",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "För att se %(communityName)s, byt till gemenskaper i dina <a>inställningar</a>",
+    "Private community": "Privat gemenskap",
+    "Public community": "Offentlig gemenskap",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s reagerade med %(content)s",
+    "Message": "Meddelande",
+    "Joining space …": "Går med i utrymme…",
+    "Message didn't send. Click for info.": "Meddelande skickades inte. Klicka för info.",
+    "Upgrade anyway": "Uppgradera ändå",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "Det här rummet är med i några utrymmen du inte är administratör för. I de utrymmena så kommer det gamla rummet fortfarande att visas, men folk kommer att uppmanas att gå med i det nya.",
+    "Before you upgrade": "Innan du uppgraderar",
+    "To join a space you'll need an invite.": "För att gå med i ett utrymme så behöver du en inbjudan.",
+    "You can also make Spaces from <a>communities</a>.": "Du kan också göra utrymmen av <a>gemenskaper</a>.",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Visa tillfälligt gemenskaper istället för utrymmen för den här sessionen. Stöd för detta kommer snart att tas bort. Detta kommer att ladda om Element.",
+    "Display Communities instead of Spaces": "Visa gemenskaper istället för utrymmen"
 }
diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json
index f559f78d7d..d5f86c1ae4 100644
--- a/src/i18n/strings/uk.json
+++ b/src/i18n/strings/uk.json
@@ -41,7 +41,7 @@
     "Camera": "Камера",
     "Advanced": "Додаткові",
     "Always show message timestamps": "Завжди показувати часові позначки повідомлень",
-    "Authentication": "Впізнавання",
+    "Authentication": "Автентифікація",
     "%(items)s and %(lastItem)s": "%(items)s та %(lastItem)s",
     "and %(count)s others...|one": "і інше...",
     "and %(count)s others...|other": "та %(count)s інші...",
@@ -54,7 +54,7 @@
     "Anyone who knows the room's link, apart from guests": "Кожний, хто знає посилання на кімнату, окрім гостей",
     "Anyone who knows the room's link, including guests": "Кожний, хто знає посилання на кімнату, включно гостей",
     "Are you sure?": "Ви впевнені?",
-    "Are you sure you want to leave the room '%(roomName)s'?": "Ви впевнені, що хочете залишити '%(roomName)s'?",
+    "Are you sure you want to leave the room '%(roomName)s'?": "Ви впевнені, що хочете вийти з «%(roomName)s»?",
     "Are you sure you want to reject the invitation?": "Ви впевнені, що ви хочете відхилити запрошення?",
     "Attachment": "Прикріплення",
     "Autoplay GIFs and videos": "Автовідтворення GIF і відео",
@@ -71,10 +71,10 @@
     "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s змінює назву кімнати на %(roomName)s.",
     "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s видалив ім'я кімнати.",
     "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s змінює тему на %(topic)s.",
-    "Email": "е-пошта",
+    "Email": "Е-пошта",
     "Email address": "Адреса е-пошти",
     "Failed to send email": "Помилка надсилання електронного листа",
-    "Edit": "Відредагувати",
+    "Edit": "Змінити",
     "Unpin Message": "Відкріпити повідомлення",
     "Register": "Зареєструватися",
     "Rooms": "Кімнати",
@@ -122,7 +122,7 @@
     "You have successfully set a password!": "Пароль успішно встановлено!",
     "An error occurred whilst saving your email notification preferences.": "Під час збереження налаштувань сповіщень е-поштою трапилася помилка.",
     "Explore Room State": "Перегляд статуса кімнати",
-    "Source URL": "Джерельне посилання",
+    "Source URL": "Початкова URL-адреса",
     "Messages sent by bot": "Повідомлення, надіслані ботом",
     "Filter results": "Відфільтрувати результати",
     "Members": "Учасники",
@@ -294,7 +294,7 @@
     "Failed to invite the following users to the %(roomName)s room:": "Не вдалося запросити таких користувачів до кімнати %(roomName)s:",
     "You need to be logged in.": "Вам потрібно увійти.",
     "You need to be able to invite users to do that.": "Щоб це зробити, вам необхідно мати можливість запрошувати людей.",
-    "Unable to create widget.": "Неможливо створити віджет.",
+    "Unable to create widget.": "Неможливо створити розширення.",
     "Missing roomId.": "Бракує ID кімнати.",
     "Failed to send request.": "Не вдалося надіслати запит.",
     "This room is not recognised.": "Кімнату не знайдено.",
@@ -310,7 +310,7 @@
     "To use it, just wait for autocomplete results to load and tab through them.": "Щоб цим скористатися, просто почекайте на підказки автодоповнення й перемикайтеся між ними клавішею TAB.",
     "Changes your display nickname": "Змінює ваш нік",
     "Invites user with given id to current room": "Запрошує користувача зі вказаним ID до кімнати",
-    "Leave room": "Залишити кімнату",
+    "Leave room": "Вийти з кімнати",
     "Kicks user with given id": "Викидає з кімнати користувача зі вказаним ID",
     "Ignores a user, hiding their messages from you": "Ігнорує користувача, приховуючи його повідомлення від вас",
     "Ignored user": "Зігнорований користувач",
@@ -358,9 +358,9 @@
     "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s зробив(-ла) майбутню історію видимою для невідомого значення (%(visibility)s).",
     "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s з %(fromPowerLevel)s до %(toPowerLevel)s",
     "%(senderName)s changed the pinned messages for the room.": "%(senderName)s змінює прикріплені повідомлення у кімнаті.",
-    "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s змінює знадіб %(widgetName)s",
-    "%(widgetName)s widget added by %(senderName)s": "%(senderName)s додав(-ла) знадіб %(widgetName)s",
-    "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s вилучив(-ла) знадіб %(widgetName)s",
+    "%(widgetName)s widget modified by %(senderName)s": "%(senderName)s змінює розширення %(widgetName)s",
+    "%(widgetName)s widget added by %(senderName)s": "%(senderName)s додає розширення %(widgetName)s",
+    "%(widgetName)s widget removed by %(senderName)s": "%(senderName)s вилучає розширення %(widgetName)s",
     "Failure to create room": "Не вдалося створити кімнату",
     "Server may be unavailable, overloaded, or you hit a bug.": "Сервер може бути недоступний, перевантажений, або ж ви натрапили на ваду.",
     "Unnamed Room": "Кімната без назви",
@@ -392,11 +392,11 @@
     "Incorrect verification code": "Неправильний код перевірки",
     "Submit": "Надіслати",
     "Phone": "Телефон",
-    "Failed to upload profile picture!": "Не вдалося відвантажити зображення профілю!",
-    "Upload new:": "Відвантажити нову:",
-    "No display name": "Немає видимого імені",
+    "Failed to upload profile picture!": "Не вдалося вивантажити зображення профілю!",
+    "Upload new:": "Вивантажити нове:",
+    "No display name": "Немає показуваного імені",
     "New passwords don't match": "Нові паролі не збігаються",
-    "Passwords can't be empty": "Пароль не може бути пустим",
+    "Passwords can't be empty": "Пароль не може бути порожнім",
     "Export E2E room keys": "Експортувати ключі наскрізного шифрування кімнат",
     "Do you want to set an email address?": "Бажаєте вказати адресу е-пошти?",
     "Current password": "Поточний пароль",
@@ -404,10 +404,10 @@
     "New Password": "Новий пароль",
     "Confirm password": "Підтвердження пароля",
     "Last seen": "Востаннє в мережі",
-    "Failed to set display name": "Не вдалося зазначити видиме ім'я",
+    "Failed to set display name": "Не вдалося вказати показуване ім'я",
     "The maximum permitted number of widgets have already been added to this room.": "Максимально дозволену кількість віджетів уже додано до цієї кімнати.",
     "Drop File Here": "Киньте файл сюди",
-    "Drop file here to upload": "Киньте файл сюди, щоб відвантажити",
+    "Drop file here to upload": "Перетягніть сюди файл, щоб вивантажити",
     " (unsupported)": " (не підтримується)",
     "Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.": "Приєднатися <voiceText>голосом</voiceText> або <videoText>відео</videoText>.",
     "Ongoing conference call%(supportedText)s.": "Триває дзвінок-конференція%(supportedText)s.",
@@ -434,7 +434,7 @@
     "Failed to change power level": "Не вдалося змінити рівень повноважень",
     "Chat with %(brand)s Bot": "Бесіда з %(brand)s-ботом",
     "Whether or not you're logged in (we don't record your username)": "Незалежно від того, увійшли ви чи ні (ми не записуємо ваше ім'я користувача)",
-    "The file '%(fileName)s' failed to upload.": "Файл '%(fileName)s' не вийшло відвантажити.",
+    "The file '%(fileName)s' failed to upload.": "Не вдалося вивантажити файл '%(fileName)s'.",
     "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Файл '%(fileName)s' перевищує ліміт розміру для відвантажень домашнього сервера",
     "The server does not support the room version specified.": "Сервер не підтримує вказану версію кімнати.",
     "Add Email Address": "Додати адресу е-пошти",
@@ -445,7 +445,7 @@
     "Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Також ви можете спробувати використати публічний сервер <code>turn.matrix.org</code>, але це буде не настільки надійно, а також цей сервер матиме змогу бачити вашу IP-адресу. Ви можете керувати цим у налаштуваннях.",
     "Try using turn.matrix.org": "Спробуйте використати turn.matrix.org",
     "Replying With Files": "Відповісти файлами",
-    "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Зараз неможливо відповісти файлом. Хочете відвантажити цей файл без відповіді?",
+    "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Зараз неможливо відповісти з файлом. Хочете вивантажити цей файл без відповіді?",
     "Name or Matrix ID": "Імʼя або Matrix ID",
     "Identity server has no terms of service": "Сервер ідентифікації не має умов надання послуг",
     "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "Щоб підтвердити адресу е-пошти або телефон ця дія потребує доступу до типового серверу ідентифікації <server />, але сервер не має жодних умов надання послуг.",
@@ -471,15 +471,15 @@
     "Use an identity server": "Використовувати сервер ідентифікації",
     "Use an identity server to invite by email. Manage in Settings.": "Використовувати сервер ідентифікації для запрошень через е-пошту. Керуйте у налаштуваннях.",
     "Unbans user with given ID": "Розблоковує користувача зі вказаним ID",
-    "Adds a custom widget by URL to the room": "Додає власний віджет до кімнати за посиланням",
-    "Please supply a https:// or http:// widget URL": "Вкажіть посилання на віджет — https:// або http://",
-    "You cannot modify widgets in this room.": "Ви не можете змінювати віджети у цій кімнаті.",
+    "Adds a custom widget by URL to the room": "Додає власне розширення до кімнати за посиланням",
+    "Please supply a https:// or http:// widget URL": "Вкажіть посилання на розширення — https:// або http://",
+    "You cannot modify widgets in this room.": "Ви не можете змінювати розширення у цій кімнаті.",
     "Forces the current outbound group session in an encrypted room to be discarded": "Примусово відкидає поточний вихідний груповий сеанс у зашифрованій кімнаті",
     "Sends the given message coloured as a rainbow": "Надсилає вказане повідомлення, розфарбоване веселкою",
     "Your %(brand)s is misconfigured": "Ваш %(brand)s налаштовано неправильно",
     "Join the discussion": "Приєднатися до обговорення",
-    "Upload": "Обрати",
-    "Upload file": "Відвантажити файл",
+    "Upload": "Вивантажити",
+    "Upload file": "Вивантажити файл",
     "Send an encrypted message…": "Надіслати зашифроване повідомлення…",
     "The conversation continues here.": "Розмова триває тут.",
     "This room has been replaced and is no longer active.": "Ця кімната була замінена і не є активною.",
@@ -496,16 +496,16 @@
     "Clear Storage and Sign Out": "Очистити сховище та вийти",
     "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Очищення сховища вашого переглядача може усунути проблему, але воно виведе вас з системи та зробить непрочитною історію ваших зашифрованих листувань.",
     "Verification Pending": "Очікується перевірка",
-    "Upload files (%(current)s of %(total)s)": "Відвантажити файли (%(current)s з %(total)s)",
-    "Upload files": "Відвантажити файли",
-    "Upload all": "Відвантажити всі",
+    "Upload files (%(current)s of %(total)s)": "Вивантажити файли (%(current)s з %(total)s)",
+    "Upload files": "Вивантажити файли",
+    "Upload all": "Вивантажити всі",
     "This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Файл <b>є надто великим</b> для відвантаження. Допустимий розмір файлів — %(limit)s, але цей файл займає %(sizeOfThisFile)s.",
     "These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "Ці файли є <b>надто великими</b> для відвантаження. Допустимий розмір файлів — %(limit)s.",
     "Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Деякі файли є <b>надто великими</b> для відвантаження. Допустимий розмір файлів — %(limit)s.",
-    "Upload %(count)s other files|other": "Відвантажити %(count)s інших файли(ів)",
+    "Upload %(count)s other files|other": "Вивантажити %(count)s інших файлів",
     "Upload Error": "Помилка відвантаження",
-    "Failed to upload image": "Не вдалось відвантажити зображення",
-    "Upload avatar": "Завантажити аватар",
+    "Failed to upload image": "Не вдалось вивантажити зображення",
+    "Upload avatar": "Вивантажити аватар",
     "For security, this session has been signed out. Please sign in again.": "З метою безпеки ваш сеанс було завершено. Увійдіть знову.",
     "Upload an avatar:": "Завантажити аватар:",
     "Custom (%(level)s)": "Власний (%(level)s)",
@@ -521,17 +521,17 @@
     "Failed to reject invitation": "Не вдалось відхилити запрошення",
     "This room is not public. You will not be able to rejoin without an invite.": "Ця кімната не є прилюдною. Ви не зможете перепід'єднатись без запрошення.",
     "Failed to leave room": "Не вдалось залишити кімнату",
-    "Can't leave Server Notices room": "Неможливо залишити кімнату Оголошення Сервера",
-    "This room is used for important messages from the Homeserver, so you cannot leave it.": "Ця кімната використовується для важливих повідомлень з домашнього сервера, тож ви не можете її залишити.",
+    "Can't leave Server Notices room": "Неможливо вийти з кімнати сповіщень сервера",
+    "This room is used for important messages from the Homeserver, so you cannot leave it.": "Ця кімната використовується для важливих повідомлень з домашнього сервера, тож ви не можете з неї вийти.",
     "Use Single Sign On to continue": "Використати Single Sign On для продовження",
     "Confirm adding this email address by using Single Sign On to prove your identity.": "Підтвердьте додавання цієї адреси е-пошти через використання Single Sign On аби довести вашу ідентичність.",
     "Single Sign On": "Єдиний вхід",
     "Confirm adding email": "Підтвердити додавання е-пошти",
-    "Click the button below to confirm adding this email address.": "Клацніть на кнопці нижче щоб підтвердити додавання цієї адреси е-пошти.",
+    "Click the button below to confirm adding this email address.": "Клацніть на кнопку внизу, щоб підтвердити додавання цієї адреси е-пошти.",
     "Confirm": "Підтвердити",
     "Confirm adding this phone number by using Single Sign On to prove your identity.": "Підтвердьте додавання цього телефонного номера через використання Single Sign On аби довести вашу ідентичність.",
     "Confirm adding phone number": "Підтвердьте додавання телефонного номера",
-    "Click the button below to confirm adding this phone number.": "Клацніть на кнопці нижче щоб підтвердити додавання цього телефонного номера.",
+    "Click the button below to confirm adding this phone number.": "Клацніть на кнопку внизу, щоб підтвердити додавання цього номера телефону.",
     "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Чи використовуєте ви %(brand)s на пристрої, де основним засобом вводження є дотик",
     "Whether you're using %(brand)s as an installed Progressive Web App": "Чи використовуєте ви %(brand)s як встановлений Progressive Web App",
     "Your user agent": "Ваш user agent",
@@ -543,7 +543,7 @@
     "Setting up keys": "Налаштовування ключів",
     "Verify this session": "Звірити цей сеанс",
     "Sign In or Create Account": "Увійти або створити обліковий запис",
-    "Use your account or create a new one to continue.": "Скористайтесь вашим обліковим записом або створіть нову, щоб продовжити.",
+    "Use your account or create a new one to continue.": "Скористайтесь вашим обліковим записом або створіть новий, щоб продовжити.",
     "Create Account": "Створити обліковий запис",
     "Sign In": "Увійти",
     "Verify all your sessions to ensure your account & messages are safe": "Звірте усі ваші сеанси, аби переконатись, що ваш обліковий запис і повідомлення у безпеці",
@@ -567,7 +567,7 @@
     "Unable to restore session": "Не вдалося відновити сеанс",
     "We encountered an error trying to restore your previous session.": "Ми натрапили на помилку, намагаючись відновити ваш попередній сеанс.",
     "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Для найкращих вражень від користування встановіть, будь ласка, <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, або <safariLink>Safari</safariLink>.",
-    "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Ваш обліковий запис має перехресно-підписувану ідентичність у таємному сховищі, але воно ще не є довіреним у цьому сеансі.",
+    "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Ваш обліковий запис має перехресне підписування особи у таємному сховищі, але цей сеанс йому ще не довіряє.",
     "in account data": "у даних облікового запису",
     "Clear notifications": "Очистити сповіщення",
     "Add an email address to configure email notifications": "Додати адресу е-пошти для налаштування поштових сповіщень",
@@ -584,7 +584,7 @@
     "Are you sure you want to deactivate your account? This is irreversible.": "Ви впевнені у тому, що бажаєте знедіяти ваш обліковий запис? Ця дія безповоротна.",
     "Confirm account deactivation": "Підтвердьте знедіювання облікового запису",
     "To continue, please enter your password:": "Щоб продовжити, введіть, будь ласка, ваш пароль:",
-    "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Ваш обліковий запис стане назавжди невикористовним. Ви не матимете змоги увійти в нього і ніхто не зможе перереєструватись під цим користувацьким ID. Це призведе до виходу вашого облікового запису з усіх кімнат та до видалення деталей вашого облікового запису з вашого серверу ідентифікації. <b>Ця дія є безповоротною.</b>",
+    "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Ви більше ніколи не зможете скористатися цим обліковим записом. Ви не зможете ввійти в нього і ніхто не зможе перереєструватись за цим користувацьким ID. Це призведе до виходу вашого облікового запису з усіх кімнат та до вилучення подробиць вашого облікового запису з вашого сервера ідентифікації. <b>Ця дія є безповоротною.</b>",
     "Verify session": "Звірити сеанс",
     "Session name": "Назва сеансу",
     "Session ID": "ID сеансу",
@@ -609,7 +609,7 @@
     "You have %(count)s unread notifications in a prior version of this room.|other": "Ви маєте %(count)s непрочитаних сповіщень у попередній версії цієї кімнати.",
     "You have %(count)s unread notifications in a prior version of this room.|one": "У вас %(count)s непрочитане сповіщення у попередній версії цієї кімнати.",
     "Deactivate user?": "Знедіяти користувача?",
-    "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Знедіювання цього користувача виведе їх з системи й унеможливить вхід у майбутньому. До того ж, вони залишать усі кімнати, в яких перебувають. Ця дія є безповоротною. Ви впевнені, що хочете знедіяти цього користувача?",
+    "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Знедіювання цього користувача виведе їх з системи й унеможливить вхід у майбутньому. До того ж вони вийдуть з усіх кімнат, у яких перебувають. Ця дія є безповоротною. Ви впевнені, що хочете знедіяти цього користувача?",
     "Deactivate user": "Знедіяти користувача",
     "Failed to deactivate user": "Не вдалось знедіяти користувача",
     "Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Знедіювання вашого облікового запису <b>типово не призводить до забуття надісланих вами повідомлень.</b> Якщо ви бажаєте, щоб ми забули ваші повідомлення, поставте прапорець внизу.",
@@ -630,7 +630,7 @@
     "Please enter verification code sent via text.": "Введіть код перевірки, надісланий у текстовому повідомленні.",
     "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "Текстове повідомлення надіслано на номер +%(msisdn)s. Введіть код перевірки з нього.",
     "Messages in this room are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Повідомлення у цій кімнаті захищені наскрізним шифруванням. Тільки ви та одержувачі мають ключі для прочитання цих повідомлень.",
-    "Messages in this room are end-to-end encrypted.": "Повідомлення у цій кімнаті наскрізно зашифровані.",
+    "Messages in this room are end-to-end encrypted.": "Повідомлення у цій кімнаті захищено наскрізним шифруванням.",
     "Messages in this room are not end-to-end encrypted.": "Повідомлення у цій кімнаті не захищено наскрізним шифруванням.",
     "Encryption enabled": "Шифрування увімкнено",
     "Messages in this room are end-to-end encrypted. Learn more & verify this user in their user profile.": "Повідомлення у цій кімнаті наскрізно зашифровані. Дізнайтеся більше та звіртеся з цим користувачем через його профіль.",
@@ -644,7 +644,7 @@
     "Unrecognised room address:": "Невпізнана адреса кімнати:",
     "Command failed": "Не вдалося виконати команду",
     "Could not find user in room": "Не вдалося знайти користувача в кімнаті",
-    "Please supply a widget URL or embed code": "Вкажіть URL або код вставки віджету",
+    "Please supply a widget URL or embed code": "Вкажіть URL або код вбудовування розширення",
     "Verifies a user, session, and pubkey tuple": "Звіряє користувача, сеанс та кортеж відкритого ключа",
     "Unknown (user, session) pair:": "Невідома пара (користувача, сеансу):",
     "Session already verified!": "Сеанс вже підтверджений!",
@@ -764,8 +764,8 @@
     "No": "НІ",
     "Review where you’re logged in": "Перевірте, де ви ввійшли",
     "Your homeserver has exceeded its user limit.": "Ваш домашній сервер перевищив свій ліміт користувачів.",
-    "Your homeserver has exceeded one of its resource limits.": "Ваш домашній сервер перевищив один із своїх ресурсних лімітів.",
-    "Contact your <a>server admin</a>.": "Зверніться до <a>адміністратора серверу</a>.",
+    "Your homeserver has exceeded one of its resource limits.": "Ваш домашній сервер перевищив одне із своїх обмежень ресурсів.",
+    "Contact your <a>server admin</a>.": "Зверніться до <a>адміністратора сервера</a>.",
     "Ok": "Гаразд",
     "Set password": "Встановити пароль",
     "Set up encryption": "Налаштування шифрування",
@@ -840,7 +840,7 @@
     "⚠ These settings are meant for advanced users.": "⚠ Ці налаштування розраховані на досвідчених користувачів.",
     "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.": "Нехтування людей реалізовано через списки правил блокування. Підписка на список блокування призведе до приховування від вас перелічених у ньому користувачів і серверів.",
     "Personal ban list": "Особистий список блокування",
-    "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.": "Ваш особистий список блокування містить усіх користувачів і сервери, повідомлення яких ви не хочете бачити. Після внесення туди першого користувача/сервера в списку кімнат з'явиться нова кімната «Мій список блокування» — не залишайте цю кімнату, щоб список блокування працював.",
+    "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.": "Ваш особистий список блокування містить усіх користувачів і сервери, повідомлення яких ви не хочете бачити. Після внесення туди першого користувача/сервера в списку кімнат з'явиться нова кімната «Мій список блокування» — не виходьте з неї, щоб список блокування працював.",
     "Server or user ID to ignore": "Сервер або ID користувача для ігнорування",
     "eg: @bot:* or example.org": "наприклад: @bot:* або example.org",
     "Ignore": "Ігнорувати",
@@ -953,7 +953,7 @@
     "Cactus": "Кактус",
     "Mushroom": "Гриб",
     "Globe": "Глобус",
-    "This bridge was provisioned by <user />.": "Цей місток був підготовленим <user />.",
+    "This bridge was provisioned by <user />.": "Цей міст було забезпечено <user />.",
     "This bridge is managed by <user />.": "Цей міст керується <user />.",
     "Workspace: %(networkName)s": "Робочий простір: %(networkName)s",
     "Channel: %(channelName)s": "Канал: %(channelName)s",
@@ -964,25 +964,25 @@
     "Gift": "Подарунок",
     "Lock": "Замок",
     "Cross-signing and secret storage are not yet set up.": "Перехресне підписування та таємне сховище ще не налагоджені.",
-    "Your homeserver does not support cross-signing.": "Ваш домашній сервер не підтримує кросс-підпис.",
+    "Your homeserver does not support cross-signing.": "Ваш домашній сервер не підтримує перехресного підписування.",
     "Cross-signing and secret storage are enabled.": "Кросс-підпис та секретне сховище дозволені.",
     "well formed": "добре сформований",
     "unexpected type": "несподіваний тип",
-    "Cross-signing public keys:": "Перехресно-підписувальні відкриті ключі:",
+    "Cross-signing public keys:": "Відкриті ключі перехресного підписування:",
     "in memory": "у пам'яті",
     "not found": "не знайдено",
-    "Cross-signing private keys:": "Приватні ключі для кросс-підпису:",
+    "Cross-signing private keys:": "Приватні ключі перехресного підписування:",
     "exists": "існує",
     "Delete sessions|other": "Видалити сеанси",
     "Delete sessions|one": "Видалити сеанс",
     "Delete %(count)s sessions|other": "Видалити %(count)s сеансів",
     "Delete %(count)s sessions|one": "Видалити %(count)s сеансів",
     "ID": "ID",
-    "Public Name": "Публічне ім'я",
+    "Public Name": "Загальнодоступне ім'я",
     " to store messages from ": " зберігання повідомлень від ",
     "rooms.": "кімнати.",
     "Manage": "Керування",
-    "Enable": "Дозволити",
+    "Enable": "Увімкнути",
     "Connecting to integration manager...": "З'єднання з менджером інтеграцій...",
     "Cannot connect to integration manager": "Не вдалося з'єднатися з менджером інтеграцій",
     "Delete Backup": "Видалити резервну копію",
@@ -994,8 +994,8 @@
     "Backup key stored: ": "Резервна копія ключа збережена ",
     "Enable audible notifications for this session": "Увімкнути звукові сповіщення для цього сеансу",
     "Save": "Зберегти",
-    "Checking server": "Перевірка серверу",
-    "Disconnect": "Відключити",
+    "Checking server": "Перевірка сервера",
+    "Disconnect": "Від'єднатися",
     "You should:": "Вам варто:",
     "Disconnect anyway": "Відключити в будь-якому випадку",
     "Identity Server (%(server)s)": "Сервер ідентифікації (%(server)s)",
@@ -1032,7 +1032,7 @@
     "Use default": "Типово",
     "Mentions & Keywords": "Згадки та ключові слова",
     "Notification options": "Параметри сповіщень",
-    "Leave Room": "Залишити кімнату",
+    "Leave Room": "Вийти з кімнати",
     "Forget Room": "Забути кімнату",
     "Favourited": "Улюблено",
     "%(count)s unread messages including mentions.|other": "%(count)s непрочитаних повідомлень включно зі згадками.",
@@ -1040,7 +1040,7 @@
     "%(count)s unread messages.|other": "%(count)s непрочитаних повідомлень.",
     "%(count)s unread messages.|one": "1 непрочитане повідомлення.",
     "Unread messages.": "Непрочитані повідомлення.",
-    "This room is public": "Ця кімната є прилюдною",
+    "This room is public": "Ця кімната загальнодоступна",
     "Show Stickers": "Показати наліпки",
     "Failed to revoke invite": "Не вдалось відкликати запрошення",
     "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Не вдалось відкликати запрошення. Сервер може мати тимчасові збої або у вас немає достатніх дозволів щоб відкликати запрошення.",
@@ -1088,7 +1088,7 @@
     "Your recovery key is in your <b>Downloads</b> folder.": "Ваш відновлювальний ключ у вашій теці <b>Завантаження</b>.",
     "Make a copy of your recovery key": "Зробити копію вашого відновлювального ключа",
     "Don't ask again": "Не запитувати знову",
-    "New Recovery Method": "Новий відновлювальний засіб",
+    "New Recovery Method": "Новий метод відновлення",
     "A new recovery passphrase and key for Secure Messages have been detected.": "Було виявлено нові відновлювальні парольну фразу та ключ від захищених повідомлень.",
     "This session is encrypting history using the new recovery method.": "Цей сеанс зашифровує історію новим відновлювальним засобом.",
     "Set up Secure Messages": "Налаштувати захищені повідомлення",
@@ -1102,7 +1102,7 @@
     "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Поліпште цей сеанс щоб уможливити звіряння інших сеансів, надаючи їм доступ до зашифрованих повідомлень та позначуючи їх довіреними для інших користувачів.",
     "Upgrade your encryption": "Поліпшити ваше шифрування",
     "Show a placeholder for removed messages": "Показувати замісну позначку замість видалених повідомлень",
-    "Show join/leave messages (invites/kicks/bans unaffected)": "Показувати повідомлення про приєднання/залишення (не впливає на запрошення/викидання/блокування)",
+    "Show join/leave messages (invites/kicks/bans unaffected)": "Показувати повідомлення про приєднання/вихід (не впливає на запрошення/викидання/блокування)",
     "Show avatar changes": "Показувати зміни личини",
     "Show display name changes": "Показувати зміни видимого імені",
     "Show read receipts sent by other users": "Показувати мітки прочитання, надіслані іншими користувачами",
@@ -1120,8 +1120,8 @@
     "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s не може безпечно локально кешувати зашифровані повідомлення під час виконання у переглядачі. Користуйтесь <desktopLink>%(brand)s Desktop</desktopLink>, в якому зашифровані повідомлення з'являються у результатах пошуку.",
     "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Ви впевнені? Ви загубите ваші зашифровані повідомлення якщо копія ключів не була зроблена коректно.",
     "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Зашифровані повідомлення захищені наскрізним шифруванням. Лише ви та отримувачі повідомлень мають ключі для їх читання.",
-    "Display Name": "Видиме ім'я",
-    "wait and try again later": "зачекайте та спопробуйте ще раз пізніше",
+    "Display Name": "Показуване ім'я",
+    "wait and try again later": "зачекати та повторити спробу пізніше",
     "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. <a>Learn more about encryption.</a>": "Якщо ви увімкнете шифрування для кімнати, його неможливо буде вимкнути. Надіслані у зашифровану кімнату повідомлення будуть прочитними тільки для учасників кімнати, натомість для сервера вони будуть непрочитними. Увімкнення шифрування може унеможливити роботу ботів та мостів. <a>Дізнатись більше про шифрування.</a>",
     "Encrypted": "Зашифроване",
     "This room is end-to-end encrypted": "Ця кімната є наскрізно зашифрованою",
@@ -1134,7 +1134,7 @@
     "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "У кімнатах з шифруванням, як у цій, попередній перегляд посилань усталено вимкнено. Це робиться, щоб гарантувати, що ваш домашній сервер (на якому генеруються перегляди) не матиме змоги збирати дані щодо посилань, які ви бачите у цій кімнаті.",
     "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "У зашифрованих кімнатах ваші повідомлення є захищеними, тож тільки ви та отримувач маєте ключі для їх розблокування.",
     "In encrypted rooms, verify all users to ensure it’s secure.": "У зашифрованих кімнатах звіряйте усіх користувачів щоб переконатись у безпеці спілкування.",
-    "Failed to copy": "Не вдалось скопіювати",
+    "Failed to copy": "Не вдалося скопіювати",
     "Your display name": "Ваше видиме ім'я",
     "COPY": "СКОПІЮВАТИ",
     "Set a display name:": "Зазначити видиме ім'я:",
@@ -1144,7 +1144,7 @@
     "Page Down": "Page Down",
     "Esc": "Esc",
     "Enter": "Enter",
-    "Space": "Пропуск",
+    "Space": "Простір",
     "End": "End",
     "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Видалення даних з цього сеансу є безповоротним. Зашифровані повідомлення будуть втрачені якщо їхні ключі не було продубльовано.",
     "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Звірте цього користувача щоб позначити його довіреним. Довіряння користувачам додає спокою якщо ви користуєтесь наскрізно зашифрованими повідомленнями.",
@@ -1163,7 +1163,7 @@
     "Show shortcuts to recently viewed rooms above the room list": "Показувати нещодавно бачені кімнати вгорі понад переліком кімнат",
     "Show hidden events in timeline": "Показувати приховані події у часоряді",
     "Show previews/thumbnails for images": "Показувати попередній перегляд зображень",
-    "Compare a unique set of emoji if you don't have a camera on either device": "Порівняйте унікальну низку емодзі якщо ви не маєте камери на жодному пристрої",
+    "Compare a unique set of emoji if you don't have a camera on either device": "Порівняйте унікальний набір емодзі якщо жоден ваш пристрій не має камери",
     "Confirm the emoji below are displayed on both sessions, in the same order:": "Підтвердьте, що емодзі внизу показано в обох сеансах в однаковому порядку:",
     "Verify this user by confirming the following emoji appear on their screen.": "Звірте цього користувача підтвердженням того, що наступні емодзі з'являються на його екрані.",
     "Emoji picker": "Обирач емодзі",
@@ -1191,7 +1191,7 @@
     "Messages containing @room": "Повідомлення, що містять @room",
     "When rooms are upgraded": "Коли кімнати поліпшено",
     "Unknown caller": "Невідомий викликач",
-    "The integration manager is offline or it cannot reach your homeserver.": "Менеджер інтеграцій непід'єднаний або не може досягти вашого домашнього сервера.",
+    "The integration manager is offline or it cannot reach your homeserver.": "Менеджер інтеграцій не під'єднаний або не може зв'язатися з вашим домашнім сервером.",
     "Enable desktop notifications for this session": "Увімкнути стільничні сповіщення для цього сеансу",
     "Profile picture": "Зображення профілю",
     "Use an Integration Manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Використовувати менеджер інтеграцій <b>%(serverName)s</b> для керування ботами, знадобами та паками наліпок.",
@@ -1235,20 +1235,20 @@
     "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Додає ( ͡° ͜ʖ ͡°) на початку текстового повідомлення",
     "about a day ago": "близько доби тому",
     "%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
-    "Unexpected server error trying to leave the room": "Виникла неочікувана помилка серверу під час спроби залишити кімнату",
+    "Unexpected server error trying to leave the room": "Під час спроби вийти з кімнати виникла неочікувана помилка сервера",
     "Unknown App": "Невідомий додаток",
     "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Надсилати <UsageDataLink>анонімну статистику користування</UsageDataLink>, що дозволяє нам вдосконалювати %(brand)s. Це використовує <PolicyLink>кукі</PolicyLink>.",
     "Set up Secure Backup": "Налаштувати захищене резервне копіювання",
     "Safeguard against losing access to encrypted messages & data": "Захистіться від втрати доступу до зашифрованих повідомлень і даних",
-    "The person who invited you already left the room.": "Особа, що вас запросила, вже залишила кімнату.",
-    "The person who invited you already left the room, or their server is offline.": "Особа, що вас запросила вже залишила кімнату, або її сервер відімкнено.",
+    "The person who invited you already left the room.": "Особа, що вас запросила, вже вийшла з кімнати.",
+    "The person who invited you already left the room, or their server is offline.": "Особа, що вас запросила вже вийшла з кімнати, або її сервер вимкнено.",
     "Change notification settings": "Змінити налаштування сповіщень",
     "Render simple counters in room header": "Показувати звичайні лічильники у заголовку кімнати",
     "Send typing notifications": "Надсилати сповіщення про набирання тексту",
     "Use a system font": "Використовувати системний шрифт",
     "System font name": "Ім’я системного шрифту",
     "Allow Peer-to-Peer for 1:1 calls": "Дозволити Peer-to-Peer для дзвінків 1:1",
-    "Enable widget screenshots on supported widgets": "Увімкнути скріншоти віджетів для віджетів, що підтримуються",
+    "Enable widget screenshots on supported widgets": "Увімкнути знімки екрана розширень для підтримуваних розширень",
     "Prompt before sending invites to potentially invalid matrix IDs": "Запитувати перед надсиланням запрошень на потенційно недійсні matrix ID",
     "Order rooms by name": "Сортувати кімнати за назвою",
     "Low bandwidth mode": "Режим для низької пропускної здатності",
@@ -1259,7 +1259,7 @@
     "Uploading logs": "Відвантаження журналів",
     "Downloading logs": "Завантаження журналів",
     "My Ban List": "Мій список блокувань",
-    "This is your list of users/servers you have blocked - don't leave the room!": "Це ваш список користувачів/серверів, які ви заблокували – не залишайте кімнату!",
+    "This is your list of users/servers you have blocked - don't leave the room!": "Це ваш список користувачів/серверів, які ви заблокували – не виходьте з кімнати!",
     "Incoming call": "Вхідний виклик",
     "The other party cancelled the verification.": "Друга сторона скасувала звірення.",
     "Verified!": "Звірено!",
@@ -1527,9 +1527,9 @@
     "Filter rooms and people": "Відфільтрувати кімнати та людей",
     "Find a room… (e.g. %(exampleRoom)s)": "Знайти кімнату… (напр. %(exampleRoom)s)",
     "Find a room…": "Знайти кімнату…",
-    "Can't find this server or its room list": "Не вдалось знайти цей сервер у переліку кімнат",
+    "Can't find this server or its room list": "Не вдалося знайти цей сервер або список його кімнат",
     "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "Якщо ви не можете знайти потрібну кімнату, попросіть запрошення або <a>Створіть нову кімнату</a> власноруч.",
-    "Cannot reach homeserver": "Не вдається зв'язатися з домашнім сервером",
+    "Cannot reach homeserver": "Не вдалося зв'язатися з домашнім сервером",
     "Ensure you have a stable internet connection, or get in touch with the server admin": "Переконайтеся, що у ваше з'єднання з Інтернетом стабільне або зв’яжіться з системним адміністратором",
     "User %(user_id)s may or may not exist": "Користувач %(user_id)s можливо існує, а можливо й ні",
     "No need for symbols, digits, or uppercase letters": "Цифри або великі букви не вимагаються",
@@ -1562,7 +1562,7 @@
     "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)sзмінили свої імена",
     "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)sзмінили свої імена %(count)s разів",
     "Error whilst fetching joined communities": "Помилка під час отримання спільнот до яких ви приєдналися",
-    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Закриті кімнати можна знайти та приєднатися до них лише за запрошенням. Загальнодоступні кімнати може знайти і приєднатись кожен з цієї спільноти.",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Приватні кімнати можна знайти та приєднатися до них лише за запрошенням. Загальнодоступні кімнати може знайти та приєднатися кожен з цієї спільноти.",
     "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Закриті кімнати можна знайти та приєднатися до них лише за запрошенням. Загальнодоступні кімнати може знайти і приєднатись кожен.",
     "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sвиходить і повертається",
     "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sвиходить і повертається %(count)s разів",
@@ -1588,7 +1588,7 @@
     "Confirm encryption setup": "Підтвердити налаштування шифрування",
     "Enable end-to-end encryption": "Увімкнути наскрізне шифрування",
     "Your server requires encryption to be enabled in private rooms.": "Ваш сервер вимагає увімкнення шифрування приватних кімнат.",
-    "Widgets do not use message encryption.": "Віджети не використовують шифрування повідомлень.",
+    "Widgets do not use message encryption.": "Розширення не використовують шифрування повідомлень.",
     "The encryption used by this room isn't supported.": "Шифрування, використане цією кімнатою не підтримується.",
     "Encryption not enabled": "Шифрування не ввімкнено",
     "Ignored attempt to disable encryption": "Знехтувані спроби вимкнути шифрування",
@@ -1633,18 +1633,18 @@
     "Sends the given message as a spoiler": "Надсилає вказане повідомлення згорненим",
     "Integration manager": "Менеджер інтеграцій",
     "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Ваш %(brand)s не дозволяє вам користуватись для цього менеджером інтеграцій. Зверніться до адміністратора.",
-    "Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Користування цим віджетом може призвести до поширення ваших даних <helpIcon /> через %(widgetDomain)s і ваш менеджер інтеграцій.",
-    "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Менеджери інтеграцій отримують дані конфігурації та можуть змінювати знадоби, надсилати запрошення у кімнати й встановлювати рівні повноважень від вашого імені.",
-    "Use an integration manager to manage bots, widgets, and sticker packs.": "Використовувати менеджер інтеграцій для керування ботами, віджетами й пакунками наліпок.",
-    "Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Використовувати менеджер інтеграцій <b>%(serverName)s</b> для керування ботами, віджетами й пакунками наліпок.",
+    "Using this widget may share data <helpIcon /> with %(widgetDomain)s & your integration manager.": "Користування цим розширенням може призвести до поширення ваших даних <helpIcon /> через %(widgetDomain)s і ваш менеджер інтеграцій.",
+    "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Менеджери інтеграцій отримують дані конфігурації та можуть змінювати розширення, надсилати запрошення у кімнати й встановлювати рівні повноважень від вашого імені.",
+    "Use an integration manager to manage bots, widgets, and sticker packs.": "Використовувати менеджер інтеграцій для керування ботами, розширеннями й пакунками наліпок.",
+    "Use an integration manager <b>(%(serverName)s)</b> to manage bots, widgets, and sticker packs.": "Використовувати менеджер інтеграцій <b>%(serverName)s</b> для керування ботами, розширеннями й пакунками наліпок.",
     "Identity server": "Сервер ідентифікації",
     "Identity server (%(server)s)": "Сервер ідентифікації (%(server)s)",
-    "Could not connect to identity server": "Не вдалося під'єднатись до сервера ідентифікації",
+    "Could not connect to identity server": "Не вдалося під'єднатися до сервера ідентифікації",
     "There was an error looking up the phone number": "Сталася помилка під час пошуку номеру телефону",
     "Unable to look up phone number": "Неможливо знайти номер телефону",
     "Not trusted": "Не довірений",
     "Trusted": "Довірений",
-    "This backup is trusted because it has been restored on this session": "Ця резервна копія є надійною, оскільки її було відновлено під час цього сеансу",
+    "This backup is trusted because it has been restored on this session": "Ця резервна копія довірена, оскільки її було відновлено у цьому сеансі",
     "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Індивідуально перевіряйте кожен сеанс, який використовується користувачем, щоб позначити його довіреним, не довіряючи пристроям перехресного підписування.",
     "To be secure, do this in person or use a trusted way to communicate.": "Для забезпечення безпеки зробіть це особисто або скористайтесь надійним способом зв'язку.",
     "You can change this at any time from room settings.": "Ви завжди можете змінити це у налаштуваннях кімнати.",
@@ -1680,8 +1680,8 @@
     "%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s відкликає запрошення %(targetName)s",
     "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s відкликає запрошення %(targetName)s: %(reason)s",
     "%(senderName)s unbanned %(targetName)s": "%(senderName)s розблоковує %(targetName)s",
-    "%(targetName)s left the room": "%(targetName)s залишає кімнату",
-    "%(targetName)s left the room: %(reason)s": "%(targetName)s залишає кімнату: %(reason)s",
+    "%(targetName)s left the room": "%(targetName)s виходить з кімнати",
+    "%(targetName)s left the room: %(reason)s": "%(targetName)s виходить з кімнати: %(reason)s",
     "%(targetName)s rejected the invitation": "%(targetName)s відхиляє запрошення",
     "%(senderName)s made no change": "%(senderName)s нічого не змінює",
     "%(senderName)s set a profile picture": "%(senderName)s встановлює зображення профілю",
@@ -1700,7 +1700,7 @@
     "Failed to transfer call": "Не вдалося переадресувати виклик",
     "Transfer Failed": "Не вдалося переадресувати",
     "Unable to transfer call": "Не вдалося переадресувати дзвінок",
-    "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Виберіть кімнати або бесіди, які потрібно додати. Це просто простір для вас, ніхто не буде поінформований. Пізніше ви можете додати більше.",
+    "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Виберіть кімнати або бесіди, які потрібно додати. Це простір лише для вас, ніхто не буде поінформований. Пізніше ви можете додати більше.",
     "Join the conference from the room information card on the right": "Приєднуйтесь до конференції з інформаційної картки кімнати праворуч",
     "Room Info": "Відомості про кімнату",
     "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Під час спроби перевірити ваше запрошення було повернуто помилку (%(errcode)s). Ви можете спробувати передати ці дані адміністратору кімнати.",
@@ -1727,7 +1727,7 @@
     "No results": "Немає результатів",
     "Application window": "Вікно застосунку",
     "Error - Mixed content": "Помилка — змішаний вміст",
-    "Widget ID": "ID віджета",
+    "Widget ID": "ID розширення",
     "%(brand)s URL": "URL-адреса %(brand)s",
     "Your theme": "Ваша тема",
     "Your user ID": "Ваш ID користувача",
@@ -1779,7 +1779,7 @@
     "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s змінює аватар %(roomName)s",
     "Change room avatar": "Змінити аватар кімнати",
     "Change the avatar of this room": "Змінює аватар цієї кімнати",
-    "Modify widgets": "Змінити віджети",
+    "Modify widgets": "Змінити розширення",
     "Notify everyone": "Сповістити всіх",
     "Remove messages sent by others": "Вилучити повідомлення надіслані іншими",
     "Kick users": "Викинути користувачів",
@@ -1835,8 +1835,8 @@
     "Forward": "Переслати",
     "Forward message": "Переслати повідомлення",
     "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Бета-версія доступна для переглядачів інтернету, настільних ПК та Android. Деякі функції можуть бути недоступні на вашому домашньому сервері.",
-    "Join the beta": "Долучитися до beta",
-    "To join %(spaceName)s, turn on the <a>Spaces beta</a>": "Щоб приєднатися до %(spaceName)s, увімкніть <a>Простори beta</a>",
+    "Join the beta": "Долучитися до бета-тестування",
+    "To join %(spaceName)s, turn on the <a>Spaces beta</a>": "Щоб приєднатися до %(spaceName)s, увімкніть <a>Простори бета</a>",
     "Spaces are a new way to group rooms and people.": "Простори — це новий спосіб згуртувати кімнати та людей.",
     "Communities are changing to Spaces": "Спільноти змінюються на Простори",
     "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Створіть спільноту, щоб об’єднати користувачів та кімнати! Створіть власну домашню сторінку, щоб позначити своє місце у всесвіті Matrix.",
@@ -1848,5 +1848,474 @@
     "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Захистіться від втрати доступу до зашифрованих повідомлень і даних створенням резервної копії ключів шифрування на своєму сервері.",
     "Secure Backup": "Безпечне резервне копіювання",
     "Give feedback.": "Надіслати відгук.",
-    "You may contact me if you have any follow up questions": "Можете зв’язатися зі мною, якщо у вас виникнуть додаткові запитання"
+    "You may contact me if you have any follow up questions": "Можете зв’язатися зі мною, якщо у вас виникнуть додаткові запитання",
+    "We sent the others, but the below people couldn't be invited to <RoomName/>": "Ми надіслали іншим, але вказаних людей, не вдалося запросити до <RoomName/>",
+    "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Ваш домашній сервер намагався відхилити спробу вашого входу. Це може бути пов'язано з занадто тривалим часом входу. Повторіть спробу. Якщо це триватиме й далі, зверніться до адміністратора домашнього сервера.",
+    "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Ваш домашній сервер був недоступний і вхід не виконано. Повторіть спробу. Якщо це триватиме й далі, зверніться до адміністратора свого домашнього сервера.",
+    "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Ми попросили переглядач запам’ятати, який домашній сервер ви використовуєте, щоб дозволити вам увійти, але, на жаль, ваш переглядач забув його. Перейдіть на сторінку входу та повторіть спробу.",
+    "You've successfully verified %(deviceName)s (%(deviceId)s)!": "Ви успішно підтвердили %(deviceName)s (%(deviceId)s)!",
+    "You've successfully verified your device!": "Ви успішно підтвердили свій пристрій!",
+    "You've successfully verified %(displayName)s!": "Ви успішно підтвердили %(displayName)s!",
+    "Almost there! Is %(displayName)s showing the same shield?": "Майже готово! Ваш %(displayName)s показує той самий щит?",
+    "Almost there! Is your other session showing the same shield?": "Майже готово! Ваш інший сеанс показує той самий щит?",
+    "Verify by scanning": "Підтвердити скануванням",
+    "Remove recent messages by %(user)s": "Вилучити останні повідомлення від %(user)s",
+    "Remove recent messages": "Видалити останні повідомлення",
+    "Edit devices": "Керувати пристроями",
+    "Home": "Домівка",
+    "New here? <a>Create an account</a>": "Вперше тут? <a>Створіть обліковий запис</a>",
+    "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "Ви можете використовувати власні опції сервера для входу на інші сервери Matrix, вказавши іншу URL-адресу домашнього сервера. Це дозволяє користуватись Element із наявним обліковим записом Matrix на іншому домашньому сервері.",
+    "Server Options": "Опції сервера",
+    "Verify your identity to access encrypted messages and prove your identity to others.": "Підтвердьте свою особу, щоб отримати доступ до зашифрованих повідомлень і довести свою справжність іншим.",
+    "Allow this widget to verify your identity": "Дозволити цьому розширенню перевіряти вашу особу",
+    "Verify this login": "Підтвердити цей вхід",
+    "Verify other login": "Підтвердити інший вхід",
+    "Use another login": "Інший обліковий запис",
+    "Use Security Key": "Використати ключ безпеки",
+    "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Без підтвердження ви не матимете доступу до всіх своїх повідомлень, а інші бачитимуть вас ненадійними.",
+    "New? <a>Create account</a>": "Вперше тут? <a>Створіть обліковий запис</a>",
+    "Forgotten your password?": "Забули свій пароль?",
+    "Forgot password?": "Забули пароль?",
+    "<userName/> invited you": "<userName/> запрошує вас",
+    "Username": "Ім'я користувача",
+    "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s або %(usernamePassword)s",
+    "Sign in with": "Увійти за допомогою",
+    "Sign in with SSO": "Увійти за допомогою SSO",
+    "Sign in": "Увійти",
+    "Got an account? <a>Sign in</a>": "Маєте обліковий запис? <a>Увійти</a>",
+    "Sign in instead": "Натомість увійти",
+    "Homeserver": "Домашній сервер",
+    "%(senderName)s unpinned a message from this room. See all pinned messages.": "%(senderName)s відкріплює повідомлення з цієї кімнати. Перегляньте всі прикріплені повідомлення.",
+    "%(senderName)s unpinned <a>a message</a> from this room. See all <b>pinned messages</b>.": "%(senderName)s відкріплює <a>повідомлення</a> з цієї кімнати. Перегляньте всі <b>прикріплені повідомлення</b>.",
+    "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s прикріплює повідомлення до цієї кімнати. Перегляньте всі прикріплені повідомлення.",
+    "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s прикріплює <a>повідомлення</a> до цієї кімнати. Перегляньте всі <b>прикріплені повідомлення</b>.",
+    "Verify this user by confirming the following number appears on their screen.": "Перевірте цього користувача, підтвердивши, що на екрані з'явилося таке число.",
+    "Verify this session by confirming the following number appears on its screen.": "Перевірте цей сеанс, підтвердивши, що на екрані з'явилося це число.",
+    "They don't match": "Вони не збігаються",
+    "They match": "Вони збігаються",
+    "Return to call": "Повернутися до виклику",
+    "Voice Call": "Голосовий виклик",
+    "Video Call": "Відеовиклик",
+    "Connecting": "З'єднання",
+    "All rooms you're in will appear in Home.": "Всі кімнати, до яких ви приєднались, з'являться в домівці.",
+    "Show all rooms in Home": "Показати всі кімнати в Домівці",
+    "Show chat effects (animations when receiving e.g. confetti)": "Показувати ефекти бесід (анімації отримання, наприклад, конфеті)",
+    "Autoplay videos": "Автовідтворення відео",
+    "Autoplay GIFs": "Автовідтворення GIF",
+    "Show stickers button": "Показати кнопку наліпок",
+    "%(senderName)s ended the call": "%(senderName)s завершує виклик",
+    "You ended the call": "Ви завершили виклик",
+    "Help space members find private rooms": "Допоможіть учасникам просторів знайти приватні кімнати",
+    "Learn more": "Докладніше",
+    "Help people in spaces to find and join private rooms": "Допоможіть людям у просторах знайти приватні кімнати та приєднатися до них",
+    "New in the Spaces beta": "Нове у бета-версії Просторів",
+    "New version of %(brand)s is available": "Доступна нова версія %(brand)s",
+    "Update %(brand)s": "Оновити %(brand)s",
+    "Check your devices": "Перевірити свої пристрої",
+    "%(deviceId)s from %(ip)s": "%(deviceId)s з %(ip)s",
+    "This homeserver has been blocked by it's administrator.": "Цей домашній сервер заблокований його адміністратором.",
+    "Use app": "Використовувати застосунок",
+    "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web — експериментальна версія на мобільних телефонах. Для зручності та найновіших можливостей, скористайтеся нашим безплатним застосунком.",
+    "Use app for a better experience": "Використовуйте застосунок для зручності",
+    "Silence call": "Тихий виклик",
+    "Sound on": "Звук увімкнено",
+    "Enable desktop notifications": "Увімкнути сповіщення стільниці",
+    "Don't miss a reply": "Не пропустіть відповідей",
+    "Review to ensure your account is safe": "Перевірте, щоб переконатися, що ваш обліковий запис у безпеці",
+    "You have unverified logins": "У вас є не підтверджені сеанси",
+    "Error leaving room": "Помилка під час виходу з кімнати",
+    "This homeserver has been blocked by its administrator.": "Цей домашній сервер заблокований адміністратором.",
+    "See when the name changes in your active room": "Бачити, коли зміниться назва активної кімнати",
+    "Change the name of your active room": "Змінити назву активної кімнати",
+    "See when the name changes in this room": "Бачити, коли зміниться назва в цій кімнаті",
+    "See when the topic changes in your active room": "Бачити, коли тема зміниться у активній кімнаті",
+    "Change the topic of your active room": "Змінити тему активної кімнати",
+    "See when the topic changes in this room": "Бачити, коли тема в цій кімнаті зміниться",
+    "Change which room, message, or user you're viewing": "Змініть кімнату, повідомлення чи користувача, які ви переглядаєте",
+    "Change which room you're viewing": "Змінити кімнату, яку ви переглядаєте",
+    "Send stickers into your active room": "Надіслати наліпки до активної кімнати",
+    "Send stickers into this room": "Надіслати наліпки до цієї кімнати",
+    "Remain on your screen while running": "Залишати на екрані під час роботи",
+    "Remain on your screen when viewing another room, when running": "Залишати на екрані під час перегляду іншої кімнати, під час роботи",
+    "%(senderName)s has updated the widget layout": "%(senderName)s оновлює макет розширення",
+    "See when the avatar changes in your active room": "Бачити, коли змінюється аватар вашої активної кімнати",
+    "Change the avatar of your active room": "Змінити аватар вашої активної кімнати",
+    "See when the avatar changes in this room": "Бачити, коли змінюється аватар цієї кімнати",
+    "Click the button below to confirm deleting these sessions.|other": "Клацніть на кнопку внизу, щоб підтвердити видалення цих сеансів.",
+    "Click the button below to confirm deleting these sessions.|one": "Клацніть на кнопку внизу, щоб підтвердити видалення цього сеансу.",
+    "Click the button below to confirm your identity.": "Клацніть на кнопку внизу, щоб підтвердити свою особу.",
+    "Confirm to continue": "Підтвердьте, щоб продовжити",
+    "Starting backup...": "Запуск резервного копіювання...",
+    "Now, let's help you get started": "Тепер допоможімо вам почати",
+    "Start authentication": "Почати автентифікацію",
+    "Start": "Почати",
+    "Start Verification": "Почати перевірку",
+    "Start chatting": "Почати спілкування",
+    "Start a new chat": "Почати нову бесіду",
+    "This is the start of <roomName/>.": "Це початок <roomName/>.",
+    "Start sharing your screen": "Почати показ екрана",
+    "Start the camera": "Запустити камеру",
+    "Scan this unique code": "Скануйте цей унікальний код",
+    "Verify this session by completing one of the following:": "Підтвердьте цей сеанс одним із запропонованих способів:",
+    "Leave %(groupName)s?": "Вийти з %(groupName)s?",
+    "Leave Community": "Вийти зі спільноти",
+    "Add a User": "Додати користувача",
+    "Add a Room": "Додати кімнату",
+    "Couldn't load page": "Не вдалося завантажити сторінку",
+    "Phone (optional)": "Телефон (не обов'язково)",
+    "That phone number doesn't look quite right, please check and try again": "Цей номер телефону не правильний. Перевірте та повторіть спробу",
+    "Enter phone number": "Введіть телефонний номер",
+    "Enter email address": "Введіть адресу е-пошти",
+    "Enter username": "Введіть ім'я користувача",
+    "Keep going...": "Продовжуйте...",
+    "Password is allowed, but unsafe": "Пароль дозволений, але небезпечний",
+    "Nice, strong password!": "Хороший надійний пароль!",
+    "Enter password": "Введіть пароль",
+    "A confirmation email has been sent to %(emailAddress)s": "Електронний лист із підтвердженням надіслано на адресу %(emailAddress)s",
+    "Please review and accept the policies of this homeserver:": "Перегляньте та прийміть політику цього домашнього сервера:",
+    "Please review and accept all of the homeserver's policies": "Перегляньте та прийміть усі правила домашнього сервера",
+    "Confirm your identity by entering your account password below.": "Підтвердьте свою особу, ввівши внизу пароль до свого облікового запису.",
+    "Open the link in the email to continue registration.": "Відкрийте посилання в електронному листі, щоб продовжити реєстрацію.",
+    "A text message has been sent to %(msisdn)s": "Текстове повідомлення надіслано на %(msisdn)s",
+    "Code": "Код",
+    "Please enter the code it contains:": "Введіть отриманий код:",
+    "Token incorrect": "Хибний токен",
+    "Country Dropdown": "Спадний список країн",
+    "User Status": "Статус користувача",
+    "Avatar": "Аватар",
+    "Tap for more info": "Торкніться, щоб переглянути подробиці",
+    "Move right": "Посунути праворуч",
+    "Move left": "Посунути ліворуч",
+    "Revoke permissions": "Відкликати дозвіл",
+    "Remove for everyone": "Прибрати для всіх",
+    "Delete widget": "Видалити розширення",
+    "Delete Widget": "Видалити розширення",
+    "View Community": "Переглянути спільноту",
+    "Move down": "Опустити",
+    "Move up": "Підняти",
+    "Set a new status...": "Установлення нового статусу...",
+    "Set status": "Налаштувати статус",
+    "Update status": "Оновити статус",
+    "Clear status": "Очистити статус",
+    "Manage & explore rooms": "Керування і перегляд кімнат",
+    "Add space": "Додати простір",
+    "Collapse reply thread": "Згорнути відповіді",
+    "Adding...": "Додавання...",
+    "Public space": "Загальнодоступний простір",
+    "Private space (invite only)": "Приватний простір (лише за запрошенням)",
+    "Space visibility": "Видимість простору",
+    "Space created": "Простір створено",
+    "Create Room": "Створити кімнату",
+    "Visible to space members": "Видима для учасників простору",
+    "Public room": "Загальнодоступна кімната",
+    "Private room (invite only)": "Приватна кімната (лише за запрошенням)",
+    "Room visibility": "Видимість кімнати",
+    "Topic (optional)": "Тема (не обов'язково)",
+    "Create a private room": "Створити приватну кімнату",
+    "Create a public room": "Створити загальнодоступну кімнату",
+    "Create a room in %(communityName)s": "Створити кімнату в %(communityName)s",
+    "Create a room": "Створити кімнату",
+    "Everyone in <SpaceName/> will be able to find and join this room.": "Усі в <SpaceName/> зможуть знайти та приєднатися до цієї кімнати.",
+    "Please enter a name for the room": "Введіть назву кімнати",
+    "example": "приклад",
+    "Community ID": "ID спільноти",
+    "Example": "Приклад",
+    "Community Name": "Назва спільноти",
+    "Create Community": "Створити спільноту",
+    "Something went wrong whilst creating your community": "Під час створення вашої спільноти щось пішло не так",
+    "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "ID спільноти повинне містити лише символи a-z, 0-9, або «=_-./»",
+    "Community IDs cannot be empty.": "ID спільноти не може бути порожнім.",
+    "An image will help people identify your community.": "Зображення допоможе людям ідентифікувати вашу спільноту.",
+    "Reason (optional)": "Причина (не обов'язково)",
+    "Add image (optional)": "Додати зображення (не обов'язково)",
+    "Enter name": "Ввести назву",
+    "What's the name of your community or team?": "Як називається ваша спільнота чи команда?",
+    "You can change this later if needed.": "За потреби, це можна змінити пізніше.",
+    "Use this when referencing your community to others. The community ID cannot be changed.": "Використовуйте його, коли ділитесь своєю спільнотою з іншими. ID спільноти змінити неможливо.",
+    "Community ID: +<localpart />:%(domain)s": "ID спільноти: +<localpart />:%(domain)s",
+    "Clear all data": "Очистити всі дані",
+    "Clear all data in this session?": "Очистити всі дані сеансу?",
+    "Confirm Removal": "Підтвердити вилучення",
+    "Removing…": "Вилучення…",
+    "Invite people to join %(communityName)s": "Запросити людей приєднатися до %(communityName)s",
+    "Send %(count)s invites|one": "Надіслати %(count)s запрошення",
+    "Send %(count)s invites|other": "Надіслати %(count)s запрошень",
+    "Show": "Показати",
+    "Hide": "Сховати",
+    "People you know on %(brand)s": "Люди, котрих ви знаєте у %(brand)s",
+    "Add another email": "Додати іншу адресу е-пошти",
+    "Notes": "Примітки",
+    "GitHub issue": "Обговорення на GitHub",
+    "Close dialog": "Закрити діалогове вікно",
+    "Invite anyway": "Усе одно запросити",
+    "Invite anyway and never warn me again": "Усе одно запросити й більше не попереджати",
+    "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Неможливо знайти профілі для Matrix ID, перерахованих унизу — все одно бажаєте запросити їх?",
+    "The following users may not exist": "Таких користувачів може не існувати",
+    "Try using one of the following valid address types: %(validTypesList)s.": "Спробуйте скористатися одним із таких допустимих типів адрес: %(validTypesList)s.",
+    "You have entered an invalid address.": "Ви ввели хибну адресу.",
+    "That doesn't look like a valid email address": "Це не схоже на правильну адресу е-пошти",
+    "email address": "адреса е-пошти",
+    "Adding spaces has moved.": "Додавання просторів переміщено.",
+    "Search for rooms": "Пошук кімнат",
+    "Create a new room": "Створити нову кімнату",
+    "Want to add a new room instead?": "Хочете додати нову кімнату натомість?",
+    "Add existing rooms": "Додати наявні кімнати",
+    "Space selection": "Вибір простору",
+    "Adding rooms... (%(progress)s out of %(count)s)|one": "Додавання кімнат...",
+    "Adding rooms... (%(progress)s out of %(count)s)|other": "Додавання кімнат... (%(progress)s з %(count)s)",
+    "Not all selected were added": "Не всі вибрані додано",
+    "Search for spaces": "Пошук просторів",
+    "Create a new space": "Створити новий простір",
+    "Want to add a new space instead?": "Хочете натомість цього додати новий простір?",
+    "Add existing space": "Додати наявний простір",
+    "Matrix rooms": "Кімнати Matrix",
+    "%(networkName)s rooms": "Кімнати %(networkName)s",
+    "Add a new server...": "Додати новий сервер...",
+    "Server name": "Назва сервера",
+    "Enter the name of a new server you want to explore.": "Введіть назву нового сервера, який ви хочете переглянути.",
+    "Add a new server": "Додати новий сервер",
+    "Matrix": "Matrix",
+    "Remove server": "Вилучити сервер",
+    "Are you sure you want to remove <b>%(serverName)s</b>": "Ви справді бажаєте вилучити <b>%(serverName)s</b>",
+    "Show preview": "Попередній перегляд",
+    "Resend %(unsentCount)s reaction(s)": "Повторно надіслати %(unsentCount)s реакцій",
+    "Your server": "Ваш сервер",
+    "You are not allowed to view this server's rooms list": "Вам не дозволено переглядати список кімнат цього сервера",
+    "Looks good": "Все добре",
+    "Enter a server name": "Введіть назву сервера",
+    "And %(count)s more...|other": "І ще %(count)s...",
+    "Sign in with single sign-on": "Увійти за допомогою єдиного входу",
+    "Continue with %(provider)s": "Продовжити з %(provider)s",
+    "Join millions for free on the largest public server": "Приєднуйтесь безплатно до мільйонів інших на найбільшому загальнодоступному сервері",
+    "Unable to reject invite": "Не вдалося відхилити запрошення",
+    "This address is already in use": "Ця адреса вже використовується",
+    "This address is available to use": "Ця адреса доступна",
+    "Please provide an address": "Будь ласка, вкажіть адресу",
+    "Some characters not allowed": "Деякі символи не дозволені",
+    "e.g. my-room": "наприклад, моя-кімната",
+    "Room address": "Адреса кімнати",
+    "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Не вдалося завантажити подію, на яку було надано відповідь, її або не існує, або у вас немає дозволу на її перегляд.",
+    "QR Code": "QR-код",
+    "Beta available for web, desktop and Android. Thank you for trying the beta.": "Бета-версія доступна для переглядачів, настільних ПК та Android. Дякуємо, що спробували бета-версію.",
+    "Spaces": "Простори",
+    "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Ви можете будь-коли вийти з бета-версії в налаштуваннях або натиснувши значок бета-версії, як описано вгорі.",
+    "Custom level": "Власний рівень",
+    "Matrix ID": "Matrix ID",
+    "%(featureName)s beta feedback": "%(featureName)s відгук про бета-версію",
+    "To leave the beta, visit your settings.": "Щоб вийти з бета-тестування, перейдіть до налаштувань.",
+    "Spaces is a beta feature": "Простори — це бета-функція",
+    "Beta": "Бета",
+    "Leave the beta": "Вийти з бета-тестування",
+    "[number]": "[цифра]",
+    "Upload a file": "Вивантажити файл",
+    "New line": "Новий рядок",
+    "Ctrl": "Ctrl",
+    "Super": "Super",
+    "Shift": "Shift",
+    "Alt Gr": "Правий Alt",
+    "Alt": "Alt",
+    "Autocomplete": "Автозаповнення",
+    "Room List": "Перелік кімнат",
+    "Calls": "Виклики",
+    "Navigation": "Навігація",
+    "Disable": "Вимкнути",
+    "Import": "Імпорт",
+    "File to import": "Файл для імпорту",
+    "Your Security Key": "Ваш ключ безпеки",
+    "User Autocomplete": "Автозаповнення користувача",
+    "Users": "Користувачі",
+    "Space Autocomplete": "Автозаповнення простору",
+    "Room Autocomplete": "Автозаповнення кімнати",
+    "Notification Autocomplete": "Автозаповнення сповіщення",
+    "Room Notification": "Сповіщення кімнати",
+    "Notify the whole room": "Сповістити всю кімнату",
+    "Community Autocomplete": "Автозаповнення спільноти",
+    "Command Autocomplete": "Команда автозаповнення",
+    "Commands": "Команди",
+    "Your new session is now verified. Other users will see it as trusted.": "Тепер ваша новий сеанс тепер підтверджено. Інші користувачі побачать її довіреною.",
+    "Registration Successful": "Реєстрацію успішно виконано",
+    "You can now close this window or <a>log in</a> to your new account.": "Тепер можете закрити це вікно або <a>увійти</a> до свого нового облікового запису.",
+    "<a>Log in</a> to your new account.": "<a>Увійти</a> до нового облікового запису.",
+    "Continue with previous account": "Продовжити з попереднім обліковим записом",
+    "Set a new password": "Установити новий пароль",
+    "Return to login screen": "Повернутися на сторінку входу",
+    "Send Reset Email": "Надіслати електронного листа скидання пароля",
+    "Session verified": "Сеанс підтверджено",
+    "User settings": "Користувацькі налаштування",
+    "Community settings": "Налаштування спільноти",
+    "Switch theme": "Змінити тему",
+    "Inviting...": "Запрошення...",
+    "Just me": "Лише я",
+    "Make sure the right people have access to %(name)s": "Переконайтеся, що потрібні люди мають доступ до %(name)s",
+    "Who are you working with?": "З ким ви працюєте?",
+    "Go to my space": "Перейти до мого простору",
+    "Go to my first room": "Перейти до моєї першої кімнати",
+    "It's just you at the moment, it will be even better with others.": "Зараз це лише для вас, якщо додати ще когось буде цікавіше.",
+    "Search for rooms or spaces": "Пошук кімнат або просторів",
+    "What do you want to organise?": "Що б ви хотіли організувати?",
+    "Creating rooms...": "Створення кімнат...",
+    "Skip for now": "Пропустити зараз",
+    "Failed to create initial space rooms": "Не вдалося створити початкові кімнати простору",
+    "Room name": "Назва кімнати",
+    "Support": "Підтримка",
+    "Random": "Випадковий",
+    "Welcome to <name/>": "Вітаємо у <name/>",
+    "Created from <Community />": "Створено з <Community />",
+    "To view %(spaceName)s, you need an invite": "Щоб приєднатися до %(spaceName)s, потрібне запрошення",
+    "To view %(spaceName)s, turn on the <a>Spaces beta</a>": "Щоб переглянути %(spaceName)s, увімкніть <a>Простори бета</a>",
+    "<inviter/> invites you": "<inviter/> запрошує вас",
+    "Private space": "Приватний простір",
+    "Search names and descriptions": "Шукати назви та описи",
+    "Rooms and spaces": "Кімнати й простори",
+    "Results": "Результати",
+    "You may want to try a different search or check for typos.": "Ви можете спробувати інший пошуковий запит або перевірити помилки.",
+    "No results found": "Нічого не знайдено",
+    "Your server does not support showing space hierarchies.": "Ваш сервер не підтримує показ ієрархій простору.",
+    "Mark as suggested": "Позначити рекомендованим",
+    "Mark as not suggested": "Позначити не рекомендованим",
+    "Removing...": "Вилучення...",
+    "Failed to remove some rooms. Try again later": "Не вдалося вилучити кілька кімнат. Повторіть спробу пізніше",
+    "Select a room below first": "Спочатку виберіть кімнату внизу",
+    "Suggested": "Пропоновано",
+    "This room is suggested as a good one to join": "Ця кімната пропонується як хороша для приєднання",
+    "You don't have permission": "Ви не маєте дозволу",
+    "Explore rooms in %(communityName)s": "Переглянути кімнати у %(communityName)s",
+    "No results for \"%(query)s\"": "За запитом «%(query)s» нічого не знайдено",
+    "View": "Перегляд",
+    "Preview": "Попередній перегляд",
+    "Filter all spaces": "Фільтрувати всі простори",
+    "You can select all or individual messages to retry or delete": "Ви можете вибрати всі або окремі повідомлення, щоб повторити спробу або видалити",
+    "Retry all": "Повторити надсилання всіх",
+    "Delete all": "Видалити всі",
+    "Some of your messages have not been sent": "Деякі з ваших повідомлень не надіслано",
+    "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>не створює резервну копію ваших ключів</b>, але у вас є резервна копія, з якої ви можете їх відновити.",
+    "Your keys are <b>not being backed up from this session</b>.": "<b>Резервна копія ваших ключів не створюється з цього сеансу</b>.",
+    "Back up your keys before signing out to avoid losing them.": "Створіть резервну копію ключів перед виходом, щоб не втратити їх.",
+    "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.": "Резервне копіювання ключів шифрування з даними вашого облікового запису на випадок втрати доступу до сеансів. Ваші ключі будуть захищені унікальним ключем безпеки.",
+    "Backup key stored:": "Резервну копію ключа розміщено:",
+    "Backup key cached:": "Резервну копію ключа кешовано:",
+    "This session is backing up your keys. ": "Цей сеанс створює резервну копію ваших ключів. ",
+    "Unable to load key backup status": "Не вдалося завантажити стан резервного копіювання ключа",
+    "The operation could not be completed": "Неможливо завершити операцію",
+    "Failed to save your profile": "Не вдалося зберегти ваш профіль",
+    "There was an error loading your notification settings.": "Сталася помилка під час завантаження налаштувань сповіщень.",
+    "Mentions & keywords": "Згадки та ключові слова",
+    "Global": "Глобально",
+    "New keyword": "Нове ключове слово",
+    "Keyword": "Ключове слово",
+    "Enable email notifications for %(email)s": "Увімкнути сповіщення е-поштою для %(email)s",
+    "Enable for this account": "Увімкнути для цього облікового запису",
+    "An error occurred whilst saving your notification preferences.": "Сталася помилка під час збереження налаштувань сповіщень.",
+    "Error saving notification preferences": "Помилка збереження налаштувань сповіщень",
+    "Messages containing keywords": "Повідомлення, що містять ключові слова",
+    "Message bubbles": "Бульбашки повідомлень",
+    "Modern": "Сучасний",
+    "IRC": "IRC",
+    "Message layout": "Макет повідомлення",
+    "This upgrade will allow members of selected spaces access to this room without an invite.": "Це оновлення дозволить учасникам обраних просторів доступитися до цієї кімнати без запрошення.",
+    "Space members": "Учасники простору",
+    "Anyone in a space can find and join. You can select multiple spaces.": "Будь-хто у просторі може знайти та приєднатися. Можна вибрати кілька просторів.",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "Будь-хто у <spaceName/> може знайти та приєднатися. Ви можете вибрати інші простори.",
+    "Spaces with access": "Простори з доступом",
+    "Anyone in a space can find and join. <a>Edit which spaces can access here.</a>": "Будь-хто у просторі може знайти та приєднатися. <a>Укажіть, які простори можуть отримати доступ сюди.</a>",
+    "Currently, %(count)s spaces have access|one": "На разі простір має доступ",
+    "contact the administrators of identity server <idserver />": "зв'язатися з адміністратором сервера ідентифікації <idserver />",
+    "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "перевірити плагіни переглядача на наявність будь-чого, що може заблокувати сервер ідентифікації (наприклад, Privacy Badger)",
+    "Disconnect from the identity server <idserver />?": "Від'єднатися від сервера ідентифікації <idserver />?",
+    "Disconnect identity server": "Від'єднатися від сервера ідентифікації",
+    "Disconnect from the identity server <current /> and connect to <new /> instead?": "Від'єднатися від сервера ідентифікації <current /> й натомість під'єднатися до <new />?",
+    "Change identity server": "Змінити сервер ідентифікації",
+    "Not a valid identity server (status code %(code)s)": "Хибний сервер ідентифікації (код статусу %(code)s)",
+    "Identity server URL must be HTTPS": "URL-адреса сервера ідентифікації повинна починатися з HTTPS",
+    "not ready": "не готове",
+    "ready": "готове",
+    "Secret storage:": "Таємне сховище:",
+    "Algorithm:": "Алгоритм:",
+    "Backup version:": "Версія резервної копії:",
+    "Currently, %(count)s spaces have access|other": "На разі доступ мають %(count)s просторів",
+    "& %(count)s more|one": "і ще %(count)s",
+    "& %(count)s more|other": "і ще %(count)s",
+    "Upgrade required": "Потрібне оновлення",
+    "Anyone can find and join.": "Будь-хто може знайти та приєднатися.",
+    "Only invited people can join.": "Приєднатися можуть лише запрошені люди.",
+    "Private (invite only)": "Приватно (лише за запрошенням)",
+    "Message search initialisation failed": "Не вдалося ініціалізувати пошук повідомлень",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Безпечно кешуйте зашифровані повідомлення локально, щоб вони з'являлися в результатах пошуку, використовуючи %(size)s для зберігання повідомлень з %(rooms)s кімнат.",
+    "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Безпечно кешуйте зашифровані повідомлення локально, щоб вони з'являлися в результатах пошуку, використовуючи %(size)s для зберігання повідомлень з %(rooms)s кімнат.",
+    "Confirm deleting these sessions": "Підтвердити видалення цих сеансів",
+    "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Підтвердити видалення цього сеансу скориставшись єдиним входом, щоб підтвердити свою особу.",
+    "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Підтвердити видалення цих сеансів скориставшись єдиним входом, щоб підтвердити свою особу.",
+    "Unable to load session list": "Неможливо завантажити список сеансів",
+    "Your homeserver does not support session management.": "Ваш домашній сервер не підтримує керування сеансами.",
+    "Homeserver feature support:": "Підтримка функції домашнім сервером:",
+    "User signing private key:": "Приватний ключ підпису користувача:",
+    "Self signing private key:": "Самопідписаний приватний ключ:",
+    "not found locally": "не знайдено локально",
+    "cached locally": "кешовано локально",
+    "Master private key:": "Головний приватний ключ:",
+    "not found in storage": "не знайдено у сховищі",
+    "in secret storage": "у таємному сховищі",
+    "Reset": "Скинути",
+    "Cross-signing is not set up.": "Перехресне підписування не налаштовано.",
+    "Cross-signing is ready but keys are not backed up.": "Перехресне підписування готове, але резервна копія ключів не створюється.",
+    "Cross-signing is ready for use.": "Перехресне підписування готове до користування.",
+    "Passwords don't match": "Паролі відрізняються",
+    "Channel: <channelLink/>": "Канал: <channelLink/>",
+    "Workspace: <networkLink/>": "Робочий простір: <networkLink/>",
+    "Space options": "Параметри простору",
+    "Collapse": "Згорнути",
+    "Expand": "Розгорнути",
+    "Recommended for public spaces.": "Рекомендовано для загальнодоступних просторів.",
+    "Allow people to preview your space before they join.": "Дозвольте людям переглядати ваш простір, перш ніж вони приєднаються.",
+    "Preview Space": "Попередній перегляд простору",
+    "Failed to update the visibility of this space": "Не вдалось оновити видимість цього простору",
+    "Decide who can view and join %(spaceName)s.": "Визначте хто може переглядати та приєднатися до %(spaceName)s.",
+    "Visibility": "Видимість",
+    "This may be useful for public spaces.": "Це може бути корисним для загальнодоступних просторів.",
+    "Guests can join a space without having an account.": "Гості можуть приєднатися до простору без облікового запису.",
+    "Enable guest access": "Увімкнути гостьовий доступ",
+    "Hide advanced": "Сховати розширені",
+    "Failed to update the history visibility of this space": "Не вдалося оновити видимість історії цього простору",
+    "Failed to update the guest access of this space": "Не вдалося оновити гостьовий доступ до цього простору",
+    "Leave Space": "Вийти з простору",
+    "Save Changes": "Зберегти зміни",
+    "Saving...": "Збереження...",
+    "Edit settings relating to your space.": "Змінити налаштування, що стосуються вашого простору.",
+    "Failed to save space settings.": "Не вдалося зберегти налаштування простору.",
+    "Invite with email or username": "Запросити за допомогою е-пошти або імені користувача",
+    "Copied!": "Скопійовано!",
+    "Click to copy": "Клацніть, щоб скопіювати",
+    "Collapse space panel": "Згорнути панель простору",
+    "Expand space panel": "Розгорнути панель простору",
+    "All rooms": "Усі кімнати",
+    "Show all rooms": "Показати всі кімнати",
+    "Create": "Створити",
+    "Creating...": "Створення...",
+    "You can change these anytime.": "Ви можете змінити це будь-коли.",
+    "Add some details to help people recognise it.": "Додайте якісь подробиці, щоб допомогти людям дізнатися про нього.",
+    "Your private space": "Ваш приватний простір",
+    "Your public space": "Ваш загальнодоступний простір",
+    "Go back": "Назад",
+    "To join an existing space you'll need an invite.": "Щоб приєднатися до наявного простору, вам знадобиться запрошення.",
+    "You can also create a Space from a <a>community</a>.": "Ви також можете створити простір зі <a>спільноти</a>.",
+    "Invite only, best for yourself or teams": "Лише за запрошенням, найкраще для себе чи для команди",
+    "Private": "Приватний",
+    "Open space for anyone, best for communities": "Відкритий простір для будь-кого, найкраще для спільнот",
+    "Public": "Загальнодоступний",
+    "You can change this later.": "Ви можете змінити це пізніше.",
+    "What kind of Space do you want to create?": "Який простір ви хочете створити?",
+    "Create a space": "Створити простір",
+    "Address": "Адреса",
+    "e.g. my-space": "наприклад, мій-простір",
+    "Spaces feedback": "Відгук про простори",
+    "Spaces are a new feature.": "Простори — це нова функція.",
+    "Please enter a name for the space": "Будь ласка, введіть назву простору",
+    "Description": "Опис",
+    "Name": "Назва",
+    "Delete": "Видалити",
+    "Delete avatar": "Видалити аватар",
+    "Your server isn't responding to some <a>requests</a>.": "Ваш сервер не відповідає на деякі <a>запити</a>.",
+    "Select room from the room list": "Вибрати кімнату з переліку",
+    "Collapse room list section": "Згорнути розділ з переліком кімнат",
+    "Go to Home View": "Перейти до домівки"
 }
diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json
index 62749185ac..a68e9092a5 100644
--- a/src/i18n/strings/zh_Hans.json
+++ b/src/i18n/strings/zh_Hans.json
@@ -3594,5 +3594,37 @@
     "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s将一条消息固定到此聊天室。查看所有固定信息。",
     "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s 将<a>一条消息</a>固定到此聊天室。查看所有<b>固定消息</b>。",
     "Currently, %(count)s spaces have access|one": "目前,一个空间有访问权限",
-    "& %(count)s more|one": "& 另外 %(count)s"
+    "& %(count)s more|one": "& 另外 %(count)s",
+    "Some encryption parameters have been changed.": "一些加密参数已更改。",
+    "Role in <RoomName/>": "<RoomName/> 中的角色",
+    "Explore %(spaceName)s": "探索 %(spaceName)s",
+    "Send a sticker": "发送贴纸",
+    "Reply to thread…": "回复主题帖…",
+    "Reply to encrypted thread…": "回复加密的主题帖…",
+    "Add emoji": "添加表情",
+    "Unknown failure": "未知失败",
+    "Failed to update the join rules": "未能更新加入列表",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "<spaceName/> 中的任何人都可以寻找和加入。你也可以选择其他空间。",
+    "Select the roles required to change various parts of the space": "选择改变空间各个部分所需的角色",
+    "Change description": "更改描述",
+    "Change main address for the space": "更改空间主地址",
+    "Change space name": "更改空间名称",
+    "Change space avatar": "更改空间头像",
+    "Message didn't send. Click for info.": "消息没有发送。点击查看信息。",
+    "Message": "消息",
+    "To join this Space, hide communities in your <a>preferences</a>": "要加入此空间,在你的<a>首选项</a>中隐藏社区",
+    "To view this Space, hide communities in your <a>preferences</a>": "要查看此空间,在你的<a>首选项</a>中隐藏社区",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "要加入 %(communityName)s,在<a>首选项</a>中滑动至社区",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "要查看 %(communityName)s,在<a>首选项</a>中滑动至社区",
+    "Private community": "私密社区",
+    "Public community": "公开社区",
+    "Upgrade anyway": "仍要升级",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "此聊天室位于某些不是由你管理的空间中。在这些空间中,旧聊天室仍将被展示,但人们将被提示加入新聊天室。",
+    "Before you upgrade": "在你升级前",
+    "To join a space you'll need an invite.": "要加入一个空间,你需要一个邀请。",
+    "You can also make Spaces from <a>communities</a>.": "你也可以从 <a>社区</a>创造空间。",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "在此会话中暂时展示社区而空间。此功能将在不久的将来被移除。这将重新加载 Element。",
+    "Display Communities instead of Spaces": "展示社区而非空间",
+    "Joining space …": "正在加入空间…",
+    "%(reactors)s reacted with %(content)s": "%(reactors)s 人回应了 %(content)s"
 }
diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json
index 869a9f6e75..4556a2d377 100644
--- a/src/i18n/strings/zh_Hant.json
+++ b/src/i18n/strings/zh_Hant.json
@@ -3705,5 +3705,35 @@
     "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)s 釘選了訊息到此聊天室。檢視所有已釘選的訊息。",
     "%(senderName)s pinned <a>a message</a> to this room. See all <b>pinned messages</b>.": "%(senderName)s 釘選了<a>訊息</a>到此聊天室。檢視所有<b>釘選的訊息</b>。",
     "Currently, %(count)s spaces have access|one": "目前,1 個空間可存取",
-    "& %(count)s more|one": "與其他 %(count)s 個"
+    "& %(count)s more|one": "與其他 %(count)s 個",
+    "Some encryption parameters have been changed.": "部份加密參數已變更。",
+    "Role in <RoomName/>": "<RoomName/> 中的角色",
+    "Explore %(spaceName)s": "探索 %(spaceName)s",
+    "Send a sticker": "傳送貼圖",
+    "Reply to thread…": "回覆討論串……",
+    "Reply to encrypted thread…": "回覆給已加密的討論串……",
+    "Add emoji": "新增表情符號",
+    "Unknown failure": "未知錯誤",
+    "Failed to update the join rules": "更新加入規則失敗",
+    "Select the roles required to change various parts of the space": "選取變更空間各個部份所需的角色",
+    "Change description": "變更描述",
+    "Change main address for the space": "變更空間的主要地址",
+    "Change space name": "變更空間名稱",
+    "Change space avatar": "變更空間大頭照",
+    "Anyone in <spaceName/> can find and join. You can select other spaces too.": "在 <spaceName/> 中的任何人都可以找到並加入。您也可以選取其他空間。",
+    "Message didn't send. Click for info.": "訊息未傳送。點擊以取得更多資訊。",
+    "To join this Space, hide communities in your <a>preferences</a>": "要加入此空間,請在您的<a>偏好設定</a>中隱藏社群",
+    "To view this Space, hide communities in your <a>preferences</a>": "要檢視此空間,請在您的<a>偏好設定</a>中隱藏社群",
+    "To join %(communityName)s, swap to communities in your <a>preferences</a>": "要加入 %(communityName)s,請在您的<a>偏好設定</a>中切換至社群",
+    "To view %(communityName)s, swap to communities in your <a>preferences</a>": "要檢視 %(communityName)s,請在您的<a>偏好設定</a>中切換至社群",
+    "Private community": "私人社群",
+    "Public community": "公開社群",
+    "Message": "訊息",
+    "Upgrade anyway": "無論如何都要升級",
+    "This room is in some spaces you’re not an admin of. In those spaces, the old room will still be shown, but people will be prompted to join the new one.": "此聊天室位於您不是管理員的空間。在那些空間中,舊的聊天室仍會顯示,但系統會提示人們加入新聊天室。",
+    "Before you upgrade": "在您升級前",
+    "To join a space you'll need an invite.": "若要加入空間,您必須被邀請。",
+    "You can also make Spaces from <a>communities</a>.": "您也可以從<a>社群</a>建立空間。",
+    "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "為此工作階段暫時顯示社群而非空間。對此功能的支援將在不久的將來移除。這將會重新載入 Element。",
+    "Display Communities instead of Spaces": "顯示社群而非空間"
 }
diff --git a/src/resizer/resizer.ts b/src/resizer/resizer.ts
index 0db13e1af5..aa1fb09e0c 100644
--- a/src/resizer/resizer.ts
+++ b/src/resizer/resizer.ts
@@ -61,10 +61,6 @@ export default class Resizer<C extends IConfig = IConfig> {
         },
         public readonly config?: C,
     ) {
-        if (!container) {
-            throw new Error("Resizer requires a non-null `container` arg");
-        }
-
         this.classNames = {
             handle: "resizer-handle",
             reverse: "resizer-reverse",
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 6dbefd4b8e..9fba69a63b 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -16,9 +16,9 @@ limitations under the License.
 */
 
 import { MatrixClient } from 'matrix-js-sdk/src/client';
-import React, { ReactNode } from "react";
+import { ReactNode } from "react";
 
-import { _t, _td } from '../languageHandler';
+import { _td } from '../languageHandler';
 import {
     NotificationBodyEnabledController,
     NotificationsEnabledController,
@@ -40,7 +40,6 @@ import { OrderedMultiController } from "./controllers/OrderedMultiController";
 import { Layout } from "./Layout";
 import ReducedMotionController from './controllers/ReducedMotionController';
 import IncompatibleController from "./controllers/IncompatibleController";
-import SdkConfig from "../SdkConfig";
 import PseudonymousAnalyticsController from './controllers/PseudonymousAnalyticsController';
 import NewLayoutSwitcherController from './controllers/NewLayoutSwitcherController';
 
@@ -145,44 +144,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         supportedLevels: LEVELS_FEATURE,
         default: false,
     },
-    "feature_spaces": {
-        isFeature: true,
-        displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " +
-            "Requires compatible homeserver for some features."),
-        supportedLevels: LEVELS_FEATURE,
-        default: false,
-        controller: new ReloadOnChangeController(),
-        betaInfo: {
-            title: _td("Spaces"),
-            caption: _td("Spaces are a new way to group rooms and people."),
-            disclaimer: (enabled) => {
-                if (enabled) {
-                    return <>
-                        <p>{ _t("If you leave, %(brand)s will reload with Spaces disabled. " +
-                            "Communities and custom tags will be visible again.", {
-                            brand: SdkConfig.get().brand,
-                        }) }</p>
-                        <p>{ _t("Beta available for web, desktop and Android. Thank you for trying the beta.") }</p>
-                    </>;
-                }
-
-                return <>
-                    <p>{ _t("%(brand)s will reload with Spaces enabled. " +
-                        "Communities and custom tags will be hidden.", {
-                        brand: SdkConfig.get().brand,
-                    }) }</p>
-                    <b>{ _t("You can leave the beta any time from settings or tapping on a beta badge, " +
-                        "like the one above.") }</b>
-                    <p>{ _t("Beta available for web, desktop and Android. " +
-                        "Some features may be unavailable on your homeserver.") }</p>
-                </>;
-            },
-            image: require("../../res/img/betas/spaces.png"),
-            feedbackSubheading: _td("Your feedback will help make spaces better. " +
-                "The more detail you can go into, the better."),
-            feedbackLabel: "spaces-feedback",
-        },
-    },
     "feature_dnd": {
         isFeature: true,
         displayName: _td("Show options to enable 'Do not disturb' mode"),
@@ -203,7 +164,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         ),
         supportedLevels: LEVELS_FEATURE,
         default: false,
-        controller: new IncompatibleController("feature_spaces"),
+        controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
     },
     "feature_pinning": {
         isFeature: true,
@@ -232,7 +193,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"),
         supportedLevels: LEVELS_FEATURE,
         default: false,
-        controller: new IncompatibleController("feature_spaces"),
+        controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
     },
     "feature_state_counters": {
         isFeature: true,
@@ -780,6 +741,15 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         description: _td("All rooms you're in will appear in Home."),
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
         default: false,
+        controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null),
+    },
+    "showCommunitiesInsteadOfSpaces": {
+        displayName: _td("Display Communities instead of Spaces"),
+        description: _td("Temporarily show communities instead of Spaces for this session. " +
+            "Support for this will be removed in the near future. This will reload Element."),
+        supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
+        default: false,
+        controller: new ReloadOnChangeController(),
     },
     [UIFeature.RoomHistorySettings]: {
         supportedLevels: LEVELS_UI_FEATURE,
@@ -844,7 +814,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
     [UIFeature.Communities]: {
         supportedLevels: LEVELS_UI_FEATURE,
         default: true,
-        controller: new IncompatibleController("feature_spaces"),
+        controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
     },
     [UIFeature.AdvancedSettings]: {
         supportedLevels: LEVELS_UI_FEATURE,
diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts
index c5b83cbcd0..9487feff5e 100644
--- a/src/settings/SettingsStore.ts
+++ b/src/settings/SettingsStore.ts
@@ -467,6 +467,10 @@ export default class SettingsStore {
             throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
         }
 
+        if (!SettingsStore.isEnabled(settingName)) {
+            return false;
+        }
+
         // When non-beta features are specified in the config.json, we force them as enabled or disabled.
         if (SettingsStore.isFeature(settingName) && !SETTINGS[settingName]?.betaInfo) {
             const configVal = SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, roomId, true, true);
diff --git a/src/settings/controllers/IncompatibleController.ts b/src/settings/controllers/IncompatibleController.ts
index c48ce0a60b..aa064219f9 100644
--- a/src/settings/controllers/IncompatibleController.ts
+++ b/src/settings/controllers/IncompatibleController.ts
@@ -24,7 +24,11 @@ import SettingsStore from "../SettingsStore";
  * labs flags.
  */
 export default class IncompatibleController extends SettingController {
-    public constructor(private settingName: string, private forcedValue = false) {
+    public constructor(
+        private settingName: string,
+        private forcedValue: any = false,
+        private incompatibleValue: any = true,
+    ) {
         super();
     }
 
@@ -34,13 +38,17 @@ export default class IncompatibleController extends SettingController {
         calculatedValue: any,
         calculatedAtLevel: SettingLevel,
     ): any {
-        if (this.incompatibleSettingEnabled) {
+        if (this.incompatibleSetting) {
             return this.forcedValue;
         }
         return null; // no override
     }
 
-    public get incompatibleSettingEnabled(): boolean {
-        return SettingsStore.getValue(this.settingName);
+    public get settingDisabled(): boolean {
+        return this.incompatibleSetting;
+    }
+
+    public get incompatibleSetting(): boolean {
+        return SettingsStore.getValue(this.settingName) === this.incompatibleValue;
     }
 }
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index f49d51454b..f28d279d00 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -71,7 +71,7 @@ export interface ISuggestedRoom extends IHierarchyRoom {
 const MAX_SUGGESTED_ROOMS = 20;
 
 // This setting causes the page to reload and can be costly if read frequently, so read it here only
-const spacesEnabled = SettingsStore.getValue("feature_spaces");
+const spacesEnabled = !SettingsStore.getValue("showCommunitiesInsteadOfSpaces");
 
 const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "HOME_SPACE"}`;
 
@@ -629,11 +629,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
     };
 
     private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => {
-        const membership = newMembership || room.getMyMembership();
+        const roomMembership = room.getMyMembership();
+        if (!roomMembership) {
+            // room is still being baked in the js-sdk, we'll process it at Room.myMembership instead
+            return;
+        }
+        const membership = newMembership || roomMembership;
 
         if (!room.isSpaceRoom()) {
             // this.onRoomUpdate(room);
-            this.onRoomsUpdate();
+            // this.onRoomsUpdate();
+            // ideally we only need onRoomsUpdate here but it doesn't rebuild parentMap so always adds new rooms to Home
+            this.rebuild();
 
             if (membership === "join") {
                 // the user just joined a room, remove it from the suggested list if it was there
@@ -845,10 +852,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
                 break;
 
             case Action.SwitchSpace:
-                if (payload.num === 0) {
+                // 1 is Home, 2-9 are the spaces after Home
+                if (payload.num === 1) {
                     this.setActiveSpace(null);
                 } else if (this.spacePanelSpaces.length >= payload.num) {
-                    this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]);
+                    this.setActiveSpace(this.spacePanelSpaces[payload.num - 2]);
                 }
                 break;
 
diff --git a/src/stores/ThreepidInviteStore.ts b/src/stores/ThreepidInviteStore.ts
index d0cf40941c..9b597ba877 100644
--- a/src/stores/ThreepidInviteStore.ts
+++ b/src/stores/ThreepidInviteStore.ts
@@ -16,6 +16,7 @@ limitations under the License.
 
 import EventEmitter from "events";
 import { base32 } from "rfc4648";
+import { RoomType } from "matrix-js-sdk/src/@types/event";
 
 // Dev note: the interface is split in two so we don't have to disable the
 // linter across the whole project.
@@ -53,6 +54,9 @@ export interface IOOBData {
     name?: string; // The room's name
     avatarUrl?: string; // The mxc:// avatar URL for the room
     inviterName?: string; // The display name of the person who invited us to the room
+    // eslint-disable-next-line camelcase
+    room_name?: string; // The name of the room, to be used until we are told better by the server
+    roomType?: RoomType; // The type of the room, to be used until we are told better by the server
 }
 
 const STORAGE_PREFIX = "mx_threepid_invite_";
diff --git a/src/stores/notifications/ListNotificationState.ts b/src/stores/notifications/ListNotificationState.ts
index 6c67dbdd08..97ba2bd80b 100644
--- a/src/stores/notifications/ListNotificationState.ts
+++ b/src/stores/notifications/ListNotificationState.ts
@@ -32,7 +32,7 @@ export class ListNotificationState extends NotificationState {
     }
 
     public get symbol(): string {
-        return null; // This notification state doesn't support symbols
+        return this._color === NotificationColor.Unsent ? "!" : null;
     }
 
     public setRooms(rooms: Room[]) {
diff --git a/src/stores/notifications/NotificationColor.ts b/src/stores/notifications/NotificationColor.ts
index b12f2b7c00..fadd5ac67e 100644
--- a/src/stores/notifications/NotificationColor.ts
+++ b/src/stores/notifications/NotificationColor.ts
@@ -21,4 +21,5 @@ export enum NotificationColor {
     Bold, // no badge, show as unread
     Grey, // unread notified messages
     Red,  // unread pings
+    Unsent, // some messages failed to send
 }
diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts
index 3fadbe7d7a..d0479200bd 100644
--- a/src/stores/notifications/RoomNotificationState.ts
+++ b/src/stores/notifications/RoomNotificationState.ts
@@ -24,6 +24,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
 import * as RoomNotifs from '../../RoomNotifs';
 import * as Unread from '../../Unread';
 import { NotificationState } from "./NotificationState";
+import { getUnsentMessages } from "../../components/structures/RoomStatusBar";
 
 export class RoomNotificationState extends NotificationState implements IDestroyable {
     constructor(public readonly room: Room) {
@@ -32,6 +33,7 @@ export class RoomNotificationState extends NotificationState implements IDestroy
         this.room.on("Room.timeline", this.handleRoomEventUpdate);
         this.room.on("Room.redaction", this.handleRoomEventUpdate);
         this.room.on("Room.myMembership", this.handleMembershipUpdate);
+        this.room.on("Room.localEchoUpdated", this.handleLocalEchoUpdated);
         MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
         MatrixClientPeg.get().on("accountData", this.handleAccountDataUpdate);
         this.updateNotificationState();
@@ -47,12 +49,17 @@ export class RoomNotificationState extends NotificationState implements IDestroy
         this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
         this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
         this.room.removeListener("Room.myMembership", this.handleMembershipUpdate);
+        this.room.removeListener("Room.localEchoUpdated", this.handleLocalEchoUpdated);
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
             MatrixClientPeg.get().removeListener("accountData", this.handleAccountDataUpdate);
         }
     }
 
+    private handleLocalEchoUpdated = () => {
+        this.updateNotificationState();
+    };
+
     private handleReadReceipt = (event: MatrixEvent, room: Room) => {
         if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore
         if (room.roomId !== this.room.roomId) return; // not for us - ignore
@@ -79,7 +86,12 @@ export class RoomNotificationState extends NotificationState implements IDestroy
     private updateNotificationState() {
         const snapshot = this.snapshot();
 
-        if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
+        if (getUnsentMessages(this.room).length > 0) {
+            // When there are unsent messages we show a red `!`
+            this._color = NotificationColor.Unsent;
+            this._symbol = "!";
+            this._count = 1; // not used, technically
+        } else if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
             // When muted we suppress all notification states, even if we have context on them.
             this._color = NotificationColor.None;
             this._symbol = null;
diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts
index f8eb07251b..137b2ca0f2 100644
--- a/src/stores/notifications/SpaceNotificationState.ts
+++ b/src/stores/notifications/SpaceNotificationState.ts
@@ -31,7 +31,7 @@ export class SpaceNotificationState extends NotificationState {
     }
 
     public get symbol(): string {
-        return null; // This notification state doesn't support symbols
+        return this._color === NotificationColor.Unsent ? "!" : null;
     }
 
     public setRooms(rooms: Room[]) {
@@ -54,7 +54,7 @@ export class SpaceNotificationState extends NotificationState {
     }
 
     public getFirstRoomWithNotifications() {
-        return this.rooms.find((room) => room.getUnreadNotificationCount() > 0).roomId;
+        return Object.values(this.states).find(state => state.color >= this.color)?.room.roomId;
     }
 
     public destroy() {
@@ -83,4 +83,3 @@ export class SpaceNotificationState extends NotificationState {
         this.emitIfUpdated(snapshot);
     }
 }
-
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 750034c573..c0a98185c7 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -67,7 +67,7 @@ interface IAppTileProps {
     userId: string;
     creatorUserId: string;
     waitForIframeLoad: boolean;
-    whitelistCapabilities: string[];
+    whitelistCapabilities?: string[];
     userWidget: boolean;
 }
 
diff --git a/src/utils/FormattingUtils.ts b/src/utils/FormattingUtils.ts
index b527ee7ea2..265deaed38 100644
--- a/src/utils/FormattingUtils.ts
+++ b/src/utils/FormattingUtils.ts
@@ -104,7 +104,10 @@ export function getUserNameColorClass(userId: string): string {
  * @returns {string} a string constructed by joining `items` with a comma
  * between each item, but with the last item appended as " and [lastItem]".
  */
-export function formatCommaSeparatedList(items: Array<string | JSX.Element>, itemLimit?: number): string | JSX.Element {
+export function formatCommaSeparatedList(items: string[], itemLimit?: number): string;
+export function formatCommaSeparatedList(items: JSX.Element[], itemLimit?: number): JSX.Element;
+export function formatCommaSeparatedList(items: Array<JSX.Element | string>, itemLimit?: number): JSX.Element | string;
+export function formatCommaSeparatedList(items: Array<JSX.Element | string>, itemLimit?: number): JSX.Element | string {
     const remaining = itemLimit === undefined ? 0 : Math.max(
         items.length - itemLimit, 0,
     );
@@ -112,11 +115,25 @@ export function formatCommaSeparatedList(items: Array<string | JSX.Element>, ite
         return "";
     } else if (items.length === 1) {
         return items[0];
-    } else if (remaining > 0) {
-        items = items.slice(0, itemLimit);
-        return _t("%(items)s and %(count)s others", { items: jsxJoin(items, ', '), count: remaining } );
     } else {
-        const lastItem = items.pop();
-        return _t("%(items)s and %(lastItem)s", { items: jsxJoin(items, ', '), lastItem: lastItem });
+        let lastItem;
+        if (remaining > 0) {
+            items = items.slice(0, itemLimit);
+        } else {
+            lastItem = items.pop();
+        }
+
+        let joinedItems;
+        if (items.every(e => typeof e === "string")) {
+            joinedItems = items.join(", ");
+        } else {
+            joinedItems = jsxJoin(items, ", ");
+        }
+
+        if (remaining > 0) {
+            return _t("%(items)s and %(count)s others", { items: joinedItems, count: remaining } );
+        } else {
+            return _t("%(items)s and %(lastItem)s", { items: joinedItems, lastItem });
+        }
     }
 }
diff --git a/test/PosthogAnalytics-test.ts b/test/PosthogAnalytics-test.ts
index 6cb1743051..65af8b51f3 100644
--- a/test/PosthogAnalytics-test.ts
+++ b/test/PosthogAnalytics-test.ts
@@ -136,18 +136,6 @@ describe("PosthogAnalytics", () => {
             expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
         });
 
-        it("Should pass trackRoomEvent to posthog", async () => {
-            analytics.setAnonymity(Anonymity.Pseudonymous);
-            const roomId = "42";
-            await analytics.trackRoomEvent<IRoomEvent>("jest_test_event", roomId, {
-                foo: "bar",
-            });
-            expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event");
-            expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
-            expect(fakePosthog.capture.mock.calls[0][1]["hashedRoomId"])
-                .toEqual("73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049");
-        });
-
         it("Should pass trackPseudonymousEvent() to posthog", async () => {
             analytics.setAnonymity(Anonymity.Pseudonymous);
             await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_pseudo_event", {
@@ -173,9 +161,6 @@ describe("PosthogAnalytics", () => {
             await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
                 foo: "bar",
             });
-            await analytics.trackRoomEvent<ITestRoomEvent>("room id", "foo", {
-                foo: "bar",
-            });
             await analytics.trackPageView(200);
             expect(fakePosthog.capture.mock.calls.length).toBe(0);
         });
@@ -183,31 +168,25 @@ describe("PosthogAnalytics", () => {
         it("Should pseudonymise a location of a known screen", async () => {
             const location = await getRedactedCurrentLocation(
                 "https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous);
-            expect(location).toBe(
-                `https://foo.bar/#/register/\
-a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
-bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
+            expect(location).toBe("https://foo.bar/#/register/<redacted>");
         });
 
         it("Should anonymise a location of a known screen", async () => {
             const location = await getRedactedCurrentLocation(
                 "https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous);
-            expect(location).toBe("https://foo.bar/#/register/<redacted>/<redacted>");
+            expect(location).toBe("https://foo.bar/#/register/<redacted>");
         });
 
         it("Should pseudonymise a location of an unknown screen", async () => {
             const location = await getRedactedCurrentLocation(
                 "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous);
-            expect(location).toBe(
-                `https://foo.bar/#/<redacted_screen_name>/\
-a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
-bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
+            expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
         });
 
         it("Should anonymise a location of an unknown screen", async () => {
             const location = await getRedactedCurrentLocation(
                 "https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous);
-            expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>/<redacted>");
+            expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
         });
 
         it("Should handle an empty hash", async () => {
@@ -218,15 +197,28 @@ bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
 
         it("Should identify the user to posthog if pseudonymous", async () => {
             analytics.setAnonymity(Anonymity.Pseudonymous);
-            await analytics.identifyUser("foo");
-            expect(fakePosthog.identify.mock.calls[0][0])
-                .toBe("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae");
+            class FakeClient {
+                getAccountDataFromServer = jest.fn().mockResolvedValue(null);
+                setAccountData = jest.fn().mockResolvedValue({});
+            }
+            await analytics.identifyUser(new FakeClient(), () => "analytics_id" );
+            expect(fakePosthog.identify.mock.calls[0][0]).toBe("analytics_id");
         });
 
         it("Should not identify the user to posthog if anonymous", async () => {
             analytics.setAnonymity(Anonymity.Anonymous);
-            await analytics.identifyUser("foo");
+            await analytics.identifyUser(null);
             expect(fakePosthog.identify.mock.calls.length).toBe(0);
         });
+
+        it("Should identify using the server's analytics id if present", async () => {
+            analytics.setAnonymity(Anonymity.Pseudonymous);
+            class FakeClient {
+                getAccountDataFromServer = jest.fn().mockResolvedValue({ id: "existing_analytics_id" });
+                setAccountData = jest.fn().mockResolvedValue({});
+            }
+            await analytics.identifyUser(new FakeClient(), () => "new_analytics_id" );
+            expect(fakePosthog.identify.mock.calls[0][0]).toBe("existing_analytics_id");
+        });
     });
 });
diff --git a/test/components/views/elements/ReplyThread-test.js b/test/components/views/elements/ReplyThread-test.js
new file mode 100644
index 0000000000..ee81c2f210
--- /dev/null
+++ b/test/components/views/elements/ReplyThread-test.js
@@ -0,0 +1,209 @@
+import "../../../skinned-sdk";
+import * as testUtils from '../../../test-utils';
+import ReplyThread from '../../../../src/components/views/elements/ReplyThread';
+
+describe("ReplyThread", () => {
+    describe('getParentEventId', () => {
+        it('retrieves relation reply from unedited event', () => {
+            const originalEventWithRelation = testUtils.mkEvent({
+                event: true,
+                type: "m.room.message",
+                content: {
+                    "msgtype": "m.text",
+                    "body": "> Reply to this message\n\n foo",
+                    "m.relates_to": {
+                        "m.in_reply_to": {
+                            "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
+                        },
+                    },
+                },
+                user: "some_other_user",
+                room: "room_id",
+            });
+
+            expect(ReplyThread.getParentEventId(originalEventWithRelation))
+                .toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og');
+        });
+
+        it('retrieves relation reply from original event when edited', () => {
+            const originalEventWithRelation = testUtils.mkEvent({
+                event: true,
+                type: "m.room.message",
+                content: {
+                    "msgtype": "m.text",
+                    "body": "> Reply to this message\n\n foo",
+                    "m.relates_to": {
+                        "m.in_reply_to": {
+                            "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
+                        },
+                    },
+                },
+                user: "some_other_user",
+                room: "room_id",
+            });
+
+            const editEvent = testUtils.mkEvent({
+                event: true,
+                type: "m.room.message",
+                content: {
+                    "msgtype": "m.text",
+                    "body": "> Reply to this message\n\n * foo bar",
+                    "m.new_content": {
+                        "msgtype": "m.text",
+                        "body": "foo bar",
+                    },
+                    "m.relates_to": {
+                        "rel_type": "m.replace",
+                        "event_id": originalEventWithRelation.event_id,
+                    },
+                },
+                user: "some_other_user",
+                room: "room_id",
+            });
+
+            // The edit replaces the original event
+            originalEventWithRelation.makeReplaced(editEvent);
+
+            // The relation should be pulled from the original event
+            expect(ReplyThread.getParentEventId(originalEventWithRelation))
+                .toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og');
+        });
+
+        it('retrieves relation reply from edit event when provided', () => {
+            const originalEvent = testUtils.mkEvent({
+                event: true,
+                type: "m.room.message",
+                content: {
+                    msgtype: "m.text",
+                    body: "foo",
+                },
+                user: "some_other_user",
+                room: "room_id",
+            });
+
+            const editEvent = testUtils.mkEvent({
+                event: true,
+                type: "m.room.message",
+                content: {
+                    "msgtype": "m.text",
+                    "body": "> Reply to this message\n\n * foo bar",
+                    "m.new_content": {
+                        "msgtype": "m.text",
+                        "body": "foo bar",
+                        "m.relates_to": {
+                            "m.in_reply_to": {
+                                "event_id": "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
+                            },
+                        },
+                    },
+                    "m.relates_to": {
+                        "rel_type": "m.replace",
+                        "event_id": originalEvent.event_id,
+                    },
+                },
+                user: "some_other_user",
+                room: "room_id",
+            });
+
+            // The edit replaces the original event
+            originalEvent.makeReplaced(editEvent);
+
+            // The relation should be pulled from the edit event
+            expect(ReplyThread.getParentEventId(originalEvent))
+                .toStrictEqual('$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og');
+        });
+
+        it('prefers relation reply from edit event over original event', () => {
+            const originalEventWithRelation = testUtils.mkEvent({
+                event: true,
+                type: "m.room.message",
+                content: {
+                    "msgtype": "m.text",
+                    "body": "> Reply to this message\n\n foo",
+                    "m.relates_to": {
+                        "m.in_reply_to": {
+                            "event_id": "$111",
+                        },
+                    },
+                },
+                user: "some_other_user",
+                room: "room_id",
+            });
+
+            const editEvent = testUtils.mkEvent({
+                event: true,
+                type: "m.room.message",
+                content: {
+                    "msgtype": "m.text",
+                    "body": "> Reply to this message\n\n * foo bar",
+                    "m.new_content": {
+                        "msgtype": "m.text",
+                        "body": "foo bar",
+                        "m.relates_to": {
+                            "m.in_reply_to": {
+                                "event_id": "$999",
+                            },
+                        },
+                    },
+                    "m.relates_to": {
+                        "rel_type": "m.replace",
+                        "event_id": originalEventWithRelation.event_id,
+                    },
+                },
+                user: "some_other_user",
+                room: "room_id",
+            });
+
+            // The edit replaces the original event
+            originalEventWithRelation.makeReplaced(editEvent);
+
+            // The relation should be pulled from the edit event
+            expect(ReplyThread.getParentEventId(originalEventWithRelation)).toStrictEqual('$999');
+        });
+
+        it('able to clear relation reply from original event by providing empty relation field', () => {
+            const originalEventWithRelation = testUtils.mkEvent({
+                event: true,
+                type: "m.room.message",
+                content: {
+                    "msgtype": "m.text",
+                    "body": "> Reply to this message\n\n foo",
+                    "m.relates_to": {
+                        "m.in_reply_to": {
+                            "event_id": "$111",
+                        },
+                    },
+                },
+                user: "some_other_user",
+                room: "room_id",
+            });
+
+            const editEvent = testUtils.mkEvent({
+                event: true,
+                type: "m.room.message",
+                content: {
+                    "msgtype": "m.text",
+                    "body": "> Reply to this message\n\n * foo bar",
+                    "m.new_content": {
+                        "msgtype": "m.text",
+                        "body": "foo bar",
+                        // Clear the relation from the original event
+                        "m.relates_to": {},
+                    },
+                    "m.relates_to": {
+                        "rel_type": "m.replace",
+                        "event_id": originalEventWithRelation.event_id,
+                    },
+                },
+                user: "some_other_user",
+                room: "room_id",
+            });
+
+            // The edit replaces the original event
+            originalEventWithRelation.makeReplaced(editEvent);
+
+            // The relation should be pulled from the edit event
+            expect(ReplyThread.getParentEventId(originalEventWithRelation)).toStrictEqual(undefined);
+        });
+    });
+});
diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js
index cee0fe5a47..760aee69a8 100644
--- a/test/components/views/rooms/RoomList-test.js
+++ b/test/components/views/rooms/RoomList-test.js
@@ -9,23 +9,16 @@ import sdk from '../../../skinned-sdk';
 
 import dis from '../../../../src/dispatcher/dispatcher';
 import DMRoomMap from '../../../../src/utils/DMRoomMap';
-import GroupStore from '../../../../src/stores/GroupStore';
 
 import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
 import { DefaultTagID } from "../../../../src/stores/room-list/models";
-import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
+import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
 import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
 
 function generateRoomId() {
     return '!' + Math.random().toString().slice(2, 10) + ':domain';
 }
 
-function waitForRoomListStoreUpdate() {
-    return new Promise((resolve) => {
-        RoomListStore.instance.once(LISTS_UPDATE_EVENT, () => resolve());
-    });
-}
-
 describe('RoomList', () => {
     function createRoom(opts) {
         const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
@@ -239,73 +232,6 @@ describe('RoomList', () => {
         });
     }
 
-    describe('when no tags are selected', () => {
-        itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
-    });
-
-    describe('when tags are selected', () => {
-        function setupSelectedTag() {
-            // Simulate a complete sync BEFORE dispatching anything else
-            dis.dispatch({
-                action: 'MatrixActions.sync',
-                prevState: null,
-                state: 'PREPARED',
-                matrixClient: client,
-            }, true);
-
-            // Simulate joined groups being received
-            dis.dispatch({
-                action: 'GroupActions.fetchJoinedGroups.success',
-                result: {
-                    groups: ['+group:domain'],
-                },
-            }, true);
-
-            // Simulate receiving tag ordering account data
-            dis.dispatch({
-                action: 'MatrixActions.accountData',
-                event_type: 'im.vector.web.tag_ordering',
-                event_content: {
-                    tags: ['+group:domain'],
-                },
-            }, true);
-
-            // GroupStore is not flux, mock and notify
-            GroupStore.getGroupRooms = (groupId) => {
-                return [movingRoom];
-            };
-            GroupStore._notifyListeners();
-
-            // We also have to mock the client's getGroup function for the room list to filter it.
-            // It's not smart enough to tell the difference between a real group and a template though.
-            client.getGroup = (groupId) => {
-                return { groupId };
-            };
-
-            // Select tag
-            dis.dispatch({ action: 'select_tag', tag: '+group:domain' }, true);
-        }
-
-        beforeEach(() => {
-            setupSelectedTag();
-        });
-
-        it('displays the correct rooms when the groups rooms are changed', async () => {
-            GroupStore.getGroupRooms = (groupId) => {
-                return [movingRoom, otherRoom];
-            };
-            GroupStore._notifyListeners();
-
-            await waitForRoomListStoreUpdate();
-
-            // XXX: Even though the store updated, it can take a bit before the update makes
-            // it to the components. This gives it plenty of time to figure out what to do.
-            await (new Promise(resolve => setTimeout(resolve, 500)));
-
-            expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged);
-        });
-
-        itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
-    });
+    itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
 });
 
diff --git a/test/end-to-end-tests/src/scenario.js b/test/end-to-end-tests/src/scenario.js
index c44f209bf3..b6581d4930 100644
--- a/test/end-to-end-tests/src/scenario.js
+++ b/test/end-to-end-tests/src/scenario.js
@@ -43,6 +43,9 @@ module.exports = async function scenario(createSession, restCreator) {
     console.log("create REST users:");
     const charlies = await createRestUsers(restCreator);
     await lazyLoadingScenarios(alice, bob, charlies);
+    // do spaces scenarios last as the rest of the tests may get confused by spaces
+    // XXX: disabled for now as fails in CI but succeeds locally
+    // await spacesScenarios(alice, bob);
 };
 
 async function createRestUsers(restCreator) {
diff --git a/test/end-to-end-tests/src/scenarios/spaces.js b/test/end-to-end-tests/src/scenarios/spaces.js
new file mode 100644
index 0000000000..303db65593
--- /dev/null
+++ b/test/end-to-end-tests/src/scenarios/spaces.js
@@ -0,0 +1,32 @@
+/*
+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.
+*/
+
+const { createSpace, inviteSpace } = require("../usecases/create-space");
+
+module.exports = async function spacesScenarios(alice, bob) {
+    console.log(" creating a space for spaces scenarios:");
+
+    await alice.delay(1000); // wait for dialogs to close
+    await setupSpaceUsingAliceAndInviteBob(alice, bob);
+};
+
+const space = "Test Space";
+
+async function setupSpaceUsingAliceAndInviteBob(alice, bob) {
+    await createSpace(alice, space);
+    await inviteSpace(alice, space, "@bob:localhost");
+    await bob.query(`.mx_SpaceButton[aria-label="${space}"]`); // assert invite received
+}
diff --git a/test/end-to-end-tests/src/usecases/create-space.js b/test/end-to-end-tests/src/usecases/create-space.js
new file mode 100644
index 0000000000..f1dbfa6b5a
--- /dev/null
+++ b/test/end-to-end-tests/src/usecases/create-space.js
@@ -0,0 +1,80 @@
+/*
+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.
+*/
+
+async function openSpaceCreateMenu(session) {
+    const spaceCreateButton = await session.query('.mx_SpaceButton_new');
+    await spaceCreateButton.click();
+}
+
+async function createSpace(session, name, isPublic = false) {
+    session.log.step(`creates space "${name}"`);
+
+    await openSpaceCreateMenu(session);
+    const className = isPublic ? ".mx_SpaceCreateMenuType_public" : ".mx_SpaceCreateMenuType_private";
+    const visibilityButton = await session.query(className);
+    await visibilityButton.click();
+
+    const nameInput = await session.query('input[name="spaceName"]');
+    await session.replaceInputText(nameInput, name);
+
+    await session.delay(100);
+
+    const createButton = await session.query('.mx_SpaceCreateMenu_wrapper .mx_AccessibleButton_kind_primary');
+    await createButton.click();
+
+    if (!isPublic) {
+        const justMeButton = await session.query('.mx_SpaceRoomView_privateScope_justMeButton');
+        await justMeButton.click();
+        const continueButton = await session.query('.mx_AddExistingToSpace_footer .mx_AccessibleButton_kind_primary');
+        await continueButton.click();
+    } else {
+        for (let i = 0; i < 2; i++) {
+            const continueButton = await session.query('.mx_SpaceRoomView_buttons .mx_AccessibleButton_kind_primary');
+            await continueButton.click();
+        }
+    }
+
+    session.log.done();
+}
+
+async function inviteSpace(session, spaceName, userId) {
+    session.log.step(`invites "${userId}" to space "${spaceName}"`);
+
+    const spaceButton = await session.query(`.mx_SpaceButton[aria-label="${spaceName}"]`);
+    await spaceButton.click({
+        button: 'right',
+    });
+
+    const inviteButton = await session.query('[aria-label="Invite people"]');
+    await inviteButton.click();
+
+    try {
+        const button = await session.query('.mx_SpacePublicShare_inviteButton');
+        await button.click();
+    } catch (e) {
+        // ignore
+    }
+
+    const inviteTextArea = await session.query(".mx_InviteDialog_editor input");
+    await inviteTextArea.type(userId);
+    const selectUserItem = await session.query(".mx_InviteDialog_roomTile");
+    await selectUserItem.click();
+    const confirmButton = await session.query(".mx_InviteDialog_goButton");
+    await confirmButton.click();
+    session.log.done();
+}
+
+module.exports = { openSpaceCreateMenu, createSpace, inviteSpace };
diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js
index 01431197a7..83d6fd79a8 100644
--- a/test/end-to-end-tests/src/usecases/room-settings.js
+++ b/test/end-to-end-tests/src/usecases/room-settings.js
@@ -161,8 +161,8 @@ async function changeRoomSettings(session, settings) {
     if (settings.visibility) {
         session.log.step(`sets visibility to ${settings.visibility}`);
         const radios = await session.queryAll(".mx_RoomSettingsDialog label");
-        assert.equal(radios.length, 6);
-        const [inviteOnlyRoom, publicRoom] = radios;
+        assert.equal(radios.length, 7);
+        const [inviteOnlyRoom,, publicRoom] = radios;
 
         if (settings.visibility === "invite_only") {
             await inviteOnlyRoom.click();
diff --git a/test/stores/SpaceStore-setup.ts b/test/stores/SpaceStore-setup.ts
deleted file mode 100644
index 78418d45cc..0000000000
--- a/test/stores/SpaceStore-setup.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// This needs to be executed before the SpaceStore gets imported but due to ES6 import hoisting we have to do this here.
-// SpaceStore reads the SettingsStore which needs the localStorage values set at init time.
-
-localStorage.setItem("mx_labs_feature_feature_spaces", "true");
diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts
index 698bd01370..e7ca727e28 100644
--- a/test/stores/SpaceStore-test.ts
+++ b/test/stores/SpaceStore-test.ts
@@ -18,7 +18,6 @@ import { EventEmitter } from "events";
 import { EventType } from "matrix-js-sdk/src/@types/event";
 import { RoomMember } from "matrix-js-sdk/src/models/room-member";
 
-import "./SpaceStore-setup"; // enable space lab
 import "../skinned-sdk"; // Must be first for skinning to work
 import SpaceStore, {
     UPDATE_HOME_BEHAVIOUR,
diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts
index 474c279fdd..cb2394349a 100644
--- a/test/stores/room-list/SpaceWatcher-test.ts
+++ b/test/stores/room-list/SpaceWatcher-test.ts
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import "../SpaceStore-setup"; // enable space lab
 import "../../skinned-sdk"; // Must be first for skinning to work
 import { SpaceWatcher } from "../../../src/stores/room-list/SpaceWatcher";
 import type { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore";
diff --git a/yarn.lock b/yarn.lock
index e0b4a403b8..546e762224 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -25,9 +25,9 @@
     tunnel "0.0.6"
 
 "@babel/cli@^7.12.10":
-  version "7.14.8"
-  resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.14.8.tgz#fac73c0e2328a8af9fd3560c06b096bfa3730933"
-  integrity sha512-lcy6Lymft9Rpfqmrqdd4oTDdUx9ZwaAhAfywVrHG4771Pa6PPT0danJ1kDHBXYqh4HHSmIdA+nlmfxfxSDPtBg==
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.15.4.tgz#00e21e192b738dec7900c8bb36270e377217c0a4"
+  integrity sha512-9RhhQ7tgKRcSO/jI3rNLxalLSk30cHqeM8bb+nGOJTyYBDpkoXw/A9QHZ2SYjlslAt4tr90pZQGIEobwWHSIDw==
   dependencies:
     commander "^4.0.1"
     convert-source-map "^1.1.0"
@@ -47,25 +47,25 @@
   dependencies:
     "@babel/highlight" "^7.14.5"
 
-"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.7", "@babel/compat-data@^7.15.0":
+"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.15.0":
   version "7.15.0"
   resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.15.0.tgz#2dbaf8b85334796cafbb0f5793a90a2fc010b176"
   integrity sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==
 
 "@babel/core@>=7.9.0", "@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.7.5":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.0.tgz#749e57c68778b73ad8082775561f67f5196aafa8"
-  integrity sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==
+  version "7.15.5"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.5.tgz#f8ed9ace730722544609f90c9bb49162dc3bf5b9"
+  integrity sha512-pYgXxiwAgQpgM1bNkZsDEq85f0ggXMA5L7c+o3tskGMh2BunCI9QUwB9Z4jpvXUOuMdyGKiGKQiRe11VS6Jzvg==
   dependencies:
     "@babel/code-frame" "^7.14.5"
-    "@babel/generator" "^7.15.0"
-    "@babel/helper-compilation-targets" "^7.15.0"
-    "@babel/helper-module-transforms" "^7.15.0"
-    "@babel/helpers" "^7.14.8"
-    "@babel/parser" "^7.15.0"
-    "@babel/template" "^7.14.5"
-    "@babel/traverse" "^7.15.0"
-    "@babel/types" "^7.15.0"
+    "@babel/generator" "^7.15.4"
+    "@babel/helper-compilation-targets" "^7.15.4"
+    "@babel/helper-module-transforms" "^7.15.4"
+    "@babel/helpers" "^7.15.4"
+    "@babel/parser" "^7.15.5"
+    "@babel/template" "^7.15.4"
+    "@babel/traverse" "^7.15.4"
+    "@babel/types" "^7.15.4"
     convert-source-map "^1.7.0"
     debug "^4.1.0"
     gensync "^1.0.0-beta.2"
@@ -74,9 +74,9 @@
     source-map "^0.5.0"
 
 "@babel/eslint-parser@^7.12.10":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.15.0.tgz#b54f06e04d0e93aebcba99f89251e3bf0ee39f21"
-  integrity sha512-+gSPtjSBxOZz4Uh8Ggqu7HbfpB8cT1LwW0DnVVLZEJvzXauiD0Di3zszcBkRmfGGrLdYeHUwcflG7i3tr9kQlw==
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.15.4.tgz#46385943726291fb3e8db99522c8099b15684387"
+  integrity sha512-hPMIAmGNbmQzXJIo2P43Zj9UhRmGev5f9nqdBFOWNGDGh6XKmjby79woBvg6y0Jur6yRfQBneDbUQ8ZVc1krFw==
   dependencies:
     eslint-scope "^5.1.1"
     eslint-visitor-keys "^2.1.0"
@@ -89,51 +89,51 @@
   dependencies:
     eslint-rule-composer "^0.3.0"
 
-"@babel/generator@^7.15.0":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.0.tgz#a7d0c172e0d814974bad5aa77ace543b97917f15"
-  integrity sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==
+"@babel/generator@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.4.tgz#85acb159a267ca6324f9793986991ee2022a05b0"
+  integrity sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==
   dependencies:
-    "@babel/types" "^7.15.0"
+    "@babel/types" "^7.15.4"
     jsesc "^2.5.1"
     source-map "^0.5.0"
 
-"@babel/helper-annotate-as-pure@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61"
-  integrity sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==
+"@babel/helper-annotate-as-pure@^7.14.5", "@babel/helper-annotate-as-pure@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz#3d0e43b00c5e49fdb6c57e421601a7a658d5f835"
+  integrity sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==
   dependencies:
-    "@babel/types" "^7.14.5"
+    "@babel/types" "^7.15.4"
 
 "@babel/helper-builder-binary-assignment-operator-visitor@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz#b939b43f8c37765443a19ae74ad8b15978e0a191"
-  integrity sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w==
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.15.4.tgz#21ad815f609b84ee0e3058676c33cf6d1670525f"
+  integrity sha512-P8o7JP2Mzi0SdC6eWr1zF+AEYvrsZa7GSY1lTayjF5XJhVH0kjLYUZPvTMflP7tBgZoe9gIhTa60QwFpqh/E0Q==
   dependencies:
-    "@babel/helper-explode-assignable-expression" "^7.14.5"
-    "@babel/types" "^7.14.5"
+    "@babel/helper-explode-assignable-expression" "^7.15.4"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5", "@babel/helper-compilation-targets@^7.15.0":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz#973df8cbd025515f3ff25db0c05efc704fa79818"
-  integrity sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A==
+"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz#cf6d94f30fbefc139123e27dd6b02f65aeedb7b9"
+  integrity sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==
   dependencies:
     "@babel/compat-data" "^7.15.0"
     "@babel/helper-validator-option" "^7.14.5"
     browserslist "^4.16.6"
     semver "^6.3.0"
 
-"@babel/helper-create-class-features-plugin@^7.14.5", "@babel/helper-create-class-features-plugin@^7.15.0":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.0.tgz#c9a137a4d137b2d0e2c649acf536d7ba1a76c0f7"
-  integrity sha512-MdmDXgvTIi4heDVX/e9EFfeGpugqm9fobBVg/iioE8kueXrOHdRDe36FAY7SnE9xXLVeYCoJR/gdrBEIHRC83Q==
+"@babel/helper-create-class-features-plugin@^7.14.5", "@babel/helper-create-class-features-plugin@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.4.tgz#7f977c17bd12a5fba363cb19bea090394bf37d2e"
+  integrity sha512-7ZmzFi+DwJx6A7mHRwbuucEYpyBwmh2Ca0RvI6z2+WLZYCqV0JOaLb+u0zbtmDicebgKBZgqbYfLaKNqSgv5Pw==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.14.5"
-    "@babel/helper-function-name" "^7.14.5"
-    "@babel/helper-member-expression-to-functions" "^7.15.0"
-    "@babel/helper-optimise-call-expression" "^7.14.5"
-    "@babel/helper-replace-supers" "^7.15.0"
-    "@babel/helper-split-export-declaration" "^7.14.5"
+    "@babel/helper-annotate-as-pure" "^7.15.4"
+    "@babel/helper-function-name" "^7.15.4"
+    "@babel/helper-member-expression-to-functions" "^7.15.4"
+    "@babel/helper-optimise-call-expression" "^7.15.4"
+    "@babel/helper-replace-supers" "^7.15.4"
+    "@babel/helper-split-export-declaration" "^7.15.4"
 
 "@babel/helper-create-regexp-features-plugin@^7.14.5":
   version "7.14.5"
@@ -157,115 +157,115 @@
     resolve "^1.14.2"
     semver "^6.1.2"
 
-"@babel/helper-explode-assignable-expression@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz#8aa72e708205c7bb643e45c73b4386cdf2a1f645"
-  integrity sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ==
+"@babel/helper-explode-assignable-expression@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.15.4.tgz#f9aec9d219f271eaf92b9f561598ca6b2682600c"
+  integrity sha512-J14f/vq8+hdC2KoWLIQSsGrC9EFBKE4NFts8pfMpymfApds+fPqR30AOUWc4tyr56h9l/GA1Sxv2q3dLZWbQ/g==
   dependencies:
-    "@babel/types" "^7.14.5"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-function-name@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz#89e2c474972f15d8e233b52ee8c480e2cfcd50c4"
-  integrity sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==
+"@babel/helper-function-name@^7.14.5", "@babel/helper-function-name@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz#845744dafc4381a4a5fb6afa6c3d36f98a787ebc"
+  integrity sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==
   dependencies:
-    "@babel/helper-get-function-arity" "^7.14.5"
-    "@babel/template" "^7.14.5"
-    "@babel/types" "^7.14.5"
+    "@babel/helper-get-function-arity" "^7.15.4"
+    "@babel/template" "^7.15.4"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-get-function-arity@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz#25fbfa579b0937eee1f3b805ece4ce398c431815"
-  integrity sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==
+"@babel/helper-get-function-arity@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz#098818934a137fce78b536a3e015864be1e2879b"
+  integrity sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==
   dependencies:
-    "@babel/types" "^7.14.5"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-hoist-variables@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz#e0dd27c33a78e577d7c8884916a3e7ef1f7c7f8d"
-  integrity sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==
+"@babel/helper-hoist-variables@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz#09993a3259c0e918f99d104261dfdfc033f178df"
+  integrity sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==
   dependencies:
-    "@babel/types" "^7.14.5"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-member-expression-to-functions@^7.15.0":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz#0ddaf5299c8179f27f37327936553e9bba60990b"
-  integrity sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg==
+"@babel/helper-member-expression-to-functions@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz#bfd34dc9bba9824a4658b0317ec2fd571a51e6ef"
+  integrity sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==
   dependencies:
-    "@babel/types" "^7.15.0"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz#6d1a44df6a38c957aa7c312da076429f11b422f3"
-  integrity sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==
+"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5", "@babel/helper-module-imports@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz#e18007d230632dea19b47853b984476e7b4e103f"
+  integrity sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==
   dependencies:
-    "@babel/types" "^7.14.5"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.15.0":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz#679275581ea056373eddbe360e1419ef23783b08"
-  integrity sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg==
+"@babel/helper-module-transforms@^7.14.5", "@babel/helper-module-transforms@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz#962cc629a7f7f9a082dd62d0307fa75fe8788d7c"
+  integrity sha512-9fHHSGE9zTC++KuXLZcB5FKgvlV83Ox+NLUmQTawovwlJ85+QMhk1CnVk406CQVj97LaWod6KVjl2Sfgw9Aktw==
   dependencies:
-    "@babel/helper-module-imports" "^7.14.5"
-    "@babel/helper-replace-supers" "^7.15.0"
-    "@babel/helper-simple-access" "^7.14.8"
-    "@babel/helper-split-export-declaration" "^7.14.5"
+    "@babel/helper-module-imports" "^7.15.4"
+    "@babel/helper-replace-supers" "^7.15.4"
+    "@babel/helper-simple-access" "^7.15.4"
+    "@babel/helper-split-export-declaration" "^7.15.4"
     "@babel/helper-validator-identifier" "^7.14.9"
-    "@babel/template" "^7.14.5"
-    "@babel/traverse" "^7.15.0"
-    "@babel/types" "^7.15.0"
+    "@babel/template" "^7.15.4"
+    "@babel/traverse" "^7.15.4"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-optimise-call-expression@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz#f27395a8619e0665b3f0364cddb41c25d71b499c"
-  integrity sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==
+"@babel/helper-optimise-call-expression@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz#f310a5121a3b9cc52d9ab19122bd729822dee171"
+  integrity sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==
   dependencies:
-    "@babel/types" "^7.14.5"
+    "@babel/types" "^7.15.4"
 
 "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
   version "7.14.5"
   resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9"
   integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==
 
-"@babel/helper-remap-async-to-generator@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.14.5.tgz#51439c913612958f54a987a4ffc9ee587a2045d6"
-  integrity sha512-rLQKdQU+HYlxBwQIj8dk4/0ENOUEhA/Z0l4hN8BexpvmSMN9oA9EagjnhnDpNsRdWCfjwa4mn/HyBXO9yhQP6A==
+"@babel/helper-remap-async-to-generator@^7.14.5", "@babel/helper-remap-async-to-generator@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.15.4.tgz#2637c0731e4c90fbf58ac58b50b2b5a192fc970f"
+  integrity sha512-v53MxgvMK/HCwckJ1bZrq6dNKlmwlyRNYM6ypaRTdXWGOE2c1/SCa6dL/HimhPulGhZKw9W0QhREM583F/t0vQ==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.14.5"
-    "@babel/helper-wrap-function" "^7.14.5"
-    "@babel/types" "^7.14.5"
+    "@babel/helper-annotate-as-pure" "^7.15.4"
+    "@babel/helper-wrap-function" "^7.15.4"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-replace-supers@^7.14.5", "@babel/helper-replace-supers@^7.15.0":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz#ace07708f5bf746bf2e6ba99572cce79b5d4e7f4"
-  integrity sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA==
+"@babel/helper-replace-supers@^7.14.5", "@babel/helper-replace-supers@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz#52a8ab26ba918c7f6dee28628b07071ac7b7347a"
+  integrity sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==
   dependencies:
-    "@babel/helper-member-expression-to-functions" "^7.15.0"
-    "@babel/helper-optimise-call-expression" "^7.14.5"
-    "@babel/traverse" "^7.15.0"
-    "@babel/types" "^7.15.0"
+    "@babel/helper-member-expression-to-functions" "^7.15.4"
+    "@babel/helper-optimise-call-expression" "^7.15.4"
+    "@babel/traverse" "^7.15.4"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-simple-access@^7.14.8":
-  version "7.14.8"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz#82e1fec0644a7e775c74d305f212c39f8fe73924"
-  integrity sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==
+"@babel/helper-simple-access@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz#ac368905abf1de8e9781434b635d8f8674bcc13b"
+  integrity sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==
   dependencies:
-    "@babel/types" "^7.14.8"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-skip-transparent-expression-wrappers@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz#96f486ac050ca9f44b009fbe5b7d394cab3a0ee4"
-  integrity sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ==
+"@babel/helper-skip-transparent-expression-wrappers@^7.14.5", "@babel/helper-skip-transparent-expression-wrappers@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz#707dbdba1f4ad0fa34f9114fc8197aec7d5da2eb"
+  integrity sha512-BMRLsdh+D1/aap19TycS4eD1qELGrCBJwzaY9IE8LrpJtJb+H7rQkPIdsfgnMtLBA6DJls7X9z93Z4U8h7xw0A==
   dependencies:
-    "@babel/types" "^7.14.5"
+    "@babel/types" "^7.15.4"
 
-"@babel/helper-split-export-declaration@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz#22b23a54ef51c2b7605d851930c1976dd0bc693a"
-  integrity sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==
+"@babel/helper-split-export-declaration@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz#aecab92dcdbef6a10aa3b62ab204b085f776e257"
+  integrity sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==
   dependencies:
-    "@babel/types" "^7.14.5"
+    "@babel/types" "^7.15.4"
 
 "@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9":
   version "7.14.9"
@@ -277,24 +277,24 @@
   resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
   integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==
 
-"@babel/helper-wrap-function@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.14.5.tgz#5919d115bf0fe328b8a5d63bcb610f51601f2bff"
-  integrity sha512-YEdjTCq+LNuNS1WfxsDCNpgXkJaIyqco6DAelTUjT4f2KIWC1nBcaCaSdHTBqQVLnTBexBcVcFhLSU1KnYuePQ==
+"@babel/helper-wrap-function@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.15.4.tgz#6f754b2446cfaf3d612523e6ab8d79c27c3a3de7"
+  integrity sha512-Y2o+H/hRV5W8QhIfTpRIBwl57y8PrZt6JM3V8FOo5qarjshHItyH5lXlpMfBfmBefOqSCpKZs/6Dxqp0E/U+uw==
   dependencies:
-    "@babel/helper-function-name" "^7.14.5"
-    "@babel/template" "^7.14.5"
-    "@babel/traverse" "^7.14.5"
-    "@babel/types" "^7.14.5"
+    "@babel/helper-function-name" "^7.15.4"
+    "@babel/template" "^7.15.4"
+    "@babel/traverse" "^7.15.4"
+    "@babel/types" "^7.15.4"
 
-"@babel/helpers@^7.14.8":
-  version "7.15.3"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.15.3.tgz#c96838b752b95dcd525b4e741ed40bb1dc2a1357"
-  integrity sha512-HwJiz52XaS96lX+28Tnbu31VeFSQJGOeKHJeaEPQlTl7PnlhFElWPj8tUXtqFIzeN86XxXoBr+WFAyK2PPVz6g==
+"@babel/helpers@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.15.4.tgz#5f40f02050a3027121a3cf48d497c05c555eaf43"
+  integrity sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==
   dependencies:
-    "@babel/template" "^7.14.5"
-    "@babel/traverse" "^7.15.0"
-    "@babel/types" "^7.15.0"
+    "@babel/template" "^7.15.4"
+    "@babel/traverse" "^7.15.4"
+    "@babel/types" "^7.15.4"
 
 "@babel/highlight@^7.14.5":
   version "7.14.5"
@@ -305,27 +305,27 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.13.16", "@babel/parser@^7.14.5", "@babel/parser@^7.15.0":
-  version "7.15.3"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.3.tgz#3416d9bea748052cfcb63dbcc27368105b1ed862"
-  integrity sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==
+"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.13.16", "@babel/parser@^7.15.4", "@babel/parser@^7.15.5":
+  version "7.15.6"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.6.tgz#043b9aa3c303c0722e5377fef9197f4cf1796549"
+  integrity sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q==
 
-"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.14.5.tgz#4b467302e1548ed3b1be43beae2cc9cf45e0bb7e"
-  integrity sha512-ZoJS2XCKPBfTmL122iP6NM9dOg+d4lc9fFk3zxc8iDjvt8Pk4+TlsHSKhIPf6X+L5ORCdBzqMZDjL/WHj7WknQ==
+"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.15.4.tgz#dbdeabb1e80f622d9f0b583efb2999605e0a567e"
+  integrity sha512-eBnpsl9tlhPhpI10kU06JHnrYXwg3+V6CaP2idsCXNef0aeslpqyITXQ74Vfk5uHgY7IG7XP0yIH8b42KSzHog==
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
-    "@babel/helper-skip-transparent-expression-wrappers" "^7.14.5"
+    "@babel/helper-skip-transparent-expression-wrappers" "^7.15.4"
     "@babel/plugin-proposal-optional-chaining" "^7.14.5"
 
-"@babel/plugin-proposal-async-generator-functions@^7.14.9":
-  version "7.14.9"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.9.tgz#7028dc4fa21dc199bbacf98b39bab1267d0eaf9a"
-  integrity sha512-d1lnh+ZnKrFKwtTYdw320+sQWCTwgkB9fmUhNXRADA4akR6wLjaruSGnIEUjpt9HCOwTr4ynFTKu19b7rFRpmw==
+"@babel/plugin-proposal-async-generator-functions@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.15.4.tgz#f82aabe96c135d2ceaa917feb9f5fca31635277e"
+  integrity sha512-2zt2g5vTXpMC3OmK6uyjvdXptbhBXfA77XGrd3gh93zwG8lZYBLOBImiGBEG0RANu3JqKEACCz5CGk73OJROBw==
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
-    "@babel/helper-remap-async-to-generator" "^7.14.5"
+    "@babel/helper-remap-async-to-generator" "^7.15.4"
     "@babel/plugin-syntax-async-generators" "^7.8.4"
 
 "@babel/plugin-proposal-class-properties@^7.12.1", "@babel/plugin-proposal-class-properties@^7.14.5":
@@ -336,21 +336,21 @@
     "@babel/helper-create-class-features-plugin" "^7.14.5"
     "@babel/helper-plugin-utils" "^7.14.5"
 
-"@babel/plugin-proposal-class-static-block@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.5.tgz#158e9e10d449c3849ef3ecde94a03d9f1841b681"
-  integrity sha512-KBAH5ksEnYHCegqseI5N9skTdxgJdmDoAOc0uXa+4QMYKeZD0w5IARh4FMlTNtaHhbB8v+KzMdTgxMMzsIy6Yg==
+"@babel/plugin-proposal-class-static-block@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.15.4.tgz#3e7ca6128453c089e8b477a99f970c63fc1cb8d7"
+  integrity sha512-M682XWrrLNk3chXCjoPUQWOyYsB93B9z3mRyjtqqYJWDf2mfCdIYgDrA11cgNVhAQieaq6F2fn2f3wI0U4aTjA==
   dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.14.5"
+    "@babel/helper-create-class-features-plugin" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/plugin-syntax-class-static-block" "^7.14.5"
 
 "@babel/plugin-proposal-decorators@^7.12.12":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.14.5.tgz#59bc4dfc1d665b5a6749cf798ff42297ed1b2c1d"
-  integrity sha512-LYz5nvQcvYeRVjui1Ykn28i+3aUiXwQ/3MGoEy0InTaz1pJo/lAzmIDXX+BQny/oufgHzJ6vnEEiXQ8KZjEVFg==
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.15.4.tgz#fb55442bc83ab4d45dda76b91949706bf22881d2"
+  integrity sha512-WNER+YLs7avvRukEddhu5PSfSaMMimX2xBFgLQS7Bw16yrUxJGWidO9nQp+yLy9MVybg5Ba3BlhAw+BkdhpDmg==
   dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.14.5"
+    "@babel/helper-create-class-features-plugin" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/plugin-syntax-decorators" "^7.14.5"
 
@@ -410,16 +410,16 @@
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/plugin-syntax-numeric-separator" "^7.10.4"
 
-"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.14.7":
-  version "7.14.7"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz#5920a2b3df7f7901df0205974c0641b13fd9d363"
-  integrity sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g==
+"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.15.6":
+  version "7.15.6"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.15.6.tgz#ef68050c8703d07b25af402cb96cf7f34a68ed11"
+  integrity sha512-qtOHo7A1Vt+O23qEAX+GdBpqaIuD3i9VRrWgCJeq7WO6H2d14EK3q11urj5Te2MAeK97nMiIdRpwd/ST4JFbNg==
   dependencies:
-    "@babel/compat-data" "^7.14.7"
-    "@babel/helper-compilation-targets" "^7.14.5"
+    "@babel/compat-data" "^7.15.0"
+    "@babel/helper-compilation-targets" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
-    "@babel/plugin-transform-parameters" "^7.14.5"
+    "@babel/plugin-transform-parameters" "^7.15.4"
 
 "@babel/plugin-proposal-optional-catch-binding@^7.14.5":
   version "7.14.5"
@@ -446,13 +446,13 @@
     "@babel/helper-create-class-features-plugin" "^7.14.5"
     "@babel/helper-plugin-utils" "^7.14.5"
 
-"@babel/plugin-proposal-private-property-in-object@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.5.tgz#9f65a4d0493a940b4c01f8aa9d3f1894a587f636"
-  integrity sha512-62EyfyA3WA0mZiF2e2IV9mc9Ghwxcg8YTu8BS4Wss4Y3PY725OmS9M0qLORbJwLqFtGh+jiE4wAmocK2CTUK2Q==
+"@babel/plugin-proposal-private-property-in-object@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.15.4.tgz#55c5e3b4d0261fd44fe637e3f624cfb0f484e3e5"
+  integrity sha512-X0UTixkLf0PCCffxgu5/1RQyGGbgZuKoI+vXP4iSbJSYwPb7hu06omsFGBvQ9lJEvwgrxHdS8B5nbfcd8GyUNA==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.14.5"
-    "@babel/helper-create-class-features-plugin" "^7.14.5"
+    "@babel/helper-annotate-as-pure" "^7.15.4"
+    "@babel/helper-create-class-features-plugin" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
 
@@ -627,24 +627,24 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
 
-"@babel/plugin-transform-block-scoping@^7.14.5":
+"@babel/plugin-transform-block-scoping@^7.15.3":
   version "7.15.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz#94c81a6e2fc230bcce6ef537ac96a1e4d2b3afaf"
   integrity sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q==
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
 
-"@babel/plugin-transform-classes@^7.14.9":
-  version "7.14.9"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.9.tgz#2a391ffb1e5292710b00f2e2c210e1435e7d449f"
-  integrity sha512-NfZpTcxU3foGWbl4wxmZ35mTsYJy8oQocbeIMoDAGGFarAmSQlL+LWMkDx/tj6pNotpbX3rltIA4dprgAPOq5A==
+"@babel/plugin-transform-classes@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.15.4.tgz#50aee17aaf7f332ae44e3bce4c2e10534d5d3bf1"
+  integrity sha512-Yjvhex8GzBmmPQUvpXRPWQ9WnxXgAFuZSrqOK/eJlOGIXwvv8H3UEdUigl1gb/bnjTrln+e8bkZUYCBt/xYlBg==
   dependencies:
-    "@babel/helper-annotate-as-pure" "^7.14.5"
-    "@babel/helper-function-name" "^7.14.5"
-    "@babel/helper-optimise-call-expression" "^7.14.5"
+    "@babel/helper-annotate-as-pure" "^7.15.4"
+    "@babel/helper-function-name" "^7.15.4"
+    "@babel/helper-optimise-call-expression" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
-    "@babel/helper-replace-supers" "^7.14.5"
-    "@babel/helper-split-export-declaration" "^7.14.5"
+    "@babel/helper-replace-supers" "^7.15.4"
+    "@babel/helper-split-export-declaration" "^7.15.4"
     globals "^11.1.0"
 
 "@babel/plugin-transform-computed-properties@^7.14.5":
@@ -684,10 +684,10 @@
     "@babel/helper-builder-binary-assignment-operator-visitor" "^7.14.5"
     "@babel/helper-plugin-utils" "^7.14.5"
 
-"@babel/plugin-transform-for-of@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz#dae384613de8f77c196a8869cbf602a44f7fc0eb"
-  integrity sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA==
+"@babel/plugin-transform-for-of@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.15.4.tgz#25c62cce2718cfb29715f416e75d5263fb36a8c2"
+  integrity sha512-DRTY9fA751AFBDh2oxydvVm4SYevs5ILTWLs6xKXps4Re/KG5nfUkr+TdHCrRWB8C69TlzVgA9b3RmGWmgN9LA==
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
 
@@ -722,25 +722,25 @@
     "@babel/helper-plugin-utils" "^7.14.5"
     babel-plugin-dynamic-import-node "^2.3.3"
 
-"@babel/plugin-transform-modules-commonjs@^7.15.0":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.0.tgz#3305896e5835f953b5cdb363acd9e8c2219a5281"
-  integrity sha512-3H/R9s8cXcOGE8kgMlmjYYC9nqr5ELiPkJn4q0mypBrjhYQoc+5/Maq69vV4xRPWnkzZuwJPf5rArxpB/35Cig==
+"@babel/plugin-transform-modules-commonjs@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.4.tgz#8201101240eabb5a76c08ef61b2954f767b6b4c1"
+  integrity sha512-qg4DPhwG8hKp4BbVDvX1s8cohM8a6Bvptu4l6Iingq5rW+yRUAhe/YRup/YcW2zCOlrysEWVhftIcKzrEZv3sA==
   dependencies:
-    "@babel/helper-module-transforms" "^7.15.0"
+    "@babel/helper-module-transforms" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
-    "@babel/helper-simple-access" "^7.14.8"
+    "@babel/helper-simple-access" "^7.15.4"
     babel-plugin-dynamic-import-node "^2.3.3"
 
-"@babel/plugin-transform-modules-systemjs@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz#c75342ef8b30dcde4295d3401aae24e65638ed29"
-  integrity sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA==
+"@babel/plugin-transform-modules-systemjs@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.15.4.tgz#b42890c7349a78c827719f1d2d0cd38c7d268132"
+  integrity sha512-fJUnlQrl/mezMneR72CKCgtOoahqGJNVKpompKwzv3BrEXdlPspTcyxrZ1XmDTIr9PpULrgEQo3qNKp6dW7ssw==
   dependencies:
-    "@babel/helper-hoist-variables" "^7.14.5"
-    "@babel/helper-module-transforms" "^7.14.5"
+    "@babel/helper-hoist-variables" "^7.15.4"
+    "@babel/helper-module-transforms" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
-    "@babel/helper-validator-identifier" "^7.14.5"
+    "@babel/helper-validator-identifier" "^7.14.9"
     babel-plugin-dynamic-import-node "^2.3.3"
 
 "@babel/plugin-transform-modules-umd@^7.14.5":
@@ -773,10 +773,10 @@
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/helper-replace-supers" "^7.14.5"
 
-"@babel/plugin-transform-parameters@^7.14.5":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz#49662e86a1f3ddccac6363a7dfb1ff0a158afeb3"
-  integrity sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA==
+"@babel/plugin-transform-parameters@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.15.4.tgz#5f2285cc3160bf48c8502432716b48504d29ed62"
+  integrity sha512-9WB/GUTO6lvJU3XQsSr6J/WKvBC2hcs4Pew8YxZagi6GkTdniyqp8On5kqdK8MN0LMeu0mGbhPN+O049NV/9FQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.14.5"
 
@@ -883,11 +883,11 @@
     "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/plugin-transform-typescript@^7.15.0":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.15.0.tgz#553f230b9d5385018716586fc48db10dd228eb7e"
-  integrity sha512-WIIEazmngMEEHDaPTx0IZY48SaAmjVWe3TRSX7cmJXn0bEv9midFzAjxiruOWYIVf5iQ10vFx7ASDpgEO08L5w==
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.15.4.tgz#db7a062dcf8be5fc096bc0eeb40a13fbfa1fa251"
+  integrity sha512-sM1/FEjwYjXvMwu1PJStH11kJ154zd/lpY56NQJ5qH2D0mabMv1CAy/kdvS9RP4Xgfj9fBBA3JiSLdDHgXdzOA==
   dependencies:
-    "@babel/helper-create-class-features-plugin" "^7.15.0"
+    "@babel/helper-create-class-features-plugin" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/plugin-syntax-typescript" "^7.14.5"
 
@@ -907,29 +907,29 @@
     "@babel/helper-plugin-utils" "^7.14.5"
 
 "@babel/preset-env@^7.12.11":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.15.0.tgz#e2165bf16594c9c05e52517a194bf6187d6fe464"
-  integrity sha512-FhEpCNFCcWW3iZLg0L2NPE9UerdtsCR6ZcsGHUX6Om6kbCQeL5QZDqFDmeNHC6/fy6UH3jEge7K4qG5uC9In0Q==
+  version "7.15.6"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.15.6.tgz#0f3898db9d63d320f21b17380d8462779de57659"
+  integrity sha512-L+6jcGn7EWu7zqaO2uoTDjjMBW+88FXzV8KvrBl2z6MtRNxlsmUNRlZPaNNPUTgqhyC5DHNFk/2Jmra+ublZWw==
   dependencies:
     "@babel/compat-data" "^7.15.0"
-    "@babel/helper-compilation-targets" "^7.15.0"
+    "@babel/helper-compilation-targets" "^7.15.4"
     "@babel/helper-plugin-utils" "^7.14.5"
     "@babel/helper-validator-option" "^7.14.5"
-    "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.14.5"
-    "@babel/plugin-proposal-async-generator-functions" "^7.14.9"
+    "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.15.4"
+    "@babel/plugin-proposal-async-generator-functions" "^7.15.4"
     "@babel/plugin-proposal-class-properties" "^7.14.5"
-    "@babel/plugin-proposal-class-static-block" "^7.14.5"
+    "@babel/plugin-proposal-class-static-block" "^7.15.4"
     "@babel/plugin-proposal-dynamic-import" "^7.14.5"
     "@babel/plugin-proposal-export-namespace-from" "^7.14.5"
     "@babel/plugin-proposal-json-strings" "^7.14.5"
     "@babel/plugin-proposal-logical-assignment-operators" "^7.14.5"
     "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5"
     "@babel/plugin-proposal-numeric-separator" "^7.14.5"
-    "@babel/plugin-proposal-object-rest-spread" "^7.14.7"
+    "@babel/plugin-proposal-object-rest-spread" "^7.15.6"
     "@babel/plugin-proposal-optional-catch-binding" "^7.14.5"
     "@babel/plugin-proposal-optional-chaining" "^7.14.5"
     "@babel/plugin-proposal-private-methods" "^7.14.5"
-    "@babel/plugin-proposal-private-property-in-object" "^7.14.5"
+    "@babel/plugin-proposal-private-property-in-object" "^7.15.4"
     "@babel/plugin-proposal-unicode-property-regex" "^7.14.5"
     "@babel/plugin-syntax-async-generators" "^7.8.4"
     "@babel/plugin-syntax-class-properties" "^7.12.13"
@@ -948,25 +948,25 @@
     "@babel/plugin-transform-arrow-functions" "^7.14.5"
     "@babel/plugin-transform-async-to-generator" "^7.14.5"
     "@babel/plugin-transform-block-scoped-functions" "^7.14.5"
-    "@babel/plugin-transform-block-scoping" "^7.14.5"
-    "@babel/plugin-transform-classes" "^7.14.9"
+    "@babel/plugin-transform-block-scoping" "^7.15.3"
+    "@babel/plugin-transform-classes" "^7.15.4"
     "@babel/plugin-transform-computed-properties" "^7.14.5"
     "@babel/plugin-transform-destructuring" "^7.14.7"
     "@babel/plugin-transform-dotall-regex" "^7.14.5"
     "@babel/plugin-transform-duplicate-keys" "^7.14.5"
     "@babel/plugin-transform-exponentiation-operator" "^7.14.5"
-    "@babel/plugin-transform-for-of" "^7.14.5"
+    "@babel/plugin-transform-for-of" "^7.15.4"
     "@babel/plugin-transform-function-name" "^7.14.5"
     "@babel/plugin-transform-literals" "^7.14.5"
     "@babel/plugin-transform-member-expression-literals" "^7.14.5"
     "@babel/plugin-transform-modules-amd" "^7.14.5"
-    "@babel/plugin-transform-modules-commonjs" "^7.15.0"
-    "@babel/plugin-transform-modules-systemjs" "^7.14.5"
+    "@babel/plugin-transform-modules-commonjs" "^7.15.4"
+    "@babel/plugin-transform-modules-systemjs" "^7.15.4"
     "@babel/plugin-transform-modules-umd" "^7.14.5"
     "@babel/plugin-transform-named-capturing-groups-regex" "^7.14.9"
     "@babel/plugin-transform-new-target" "^7.14.5"
     "@babel/plugin-transform-object-super" "^7.14.5"
-    "@babel/plugin-transform-parameters" "^7.14.5"
+    "@babel/plugin-transform-parameters" "^7.15.4"
     "@babel/plugin-transform-property-literals" "^7.14.5"
     "@babel/plugin-transform-regenerator" "^7.14.5"
     "@babel/plugin-transform-reserved-words" "^7.14.5"
@@ -978,7 +978,7 @@
     "@babel/plugin-transform-unicode-escapes" "^7.14.5"
     "@babel/plugin-transform-unicode-regex" "^7.14.5"
     "@babel/preset-modules" "^0.1.4"
-    "@babel/types" "^7.15.0"
+    "@babel/types" "^7.15.6"
     babel-plugin-polyfill-corejs2 "^0.2.2"
     babel-plugin-polyfill-corejs3 "^0.2.2"
     babel-plugin-polyfill-regenerator "^0.2.2"
@@ -1029,40 +1029,40 @@
     source-map-support "^0.5.16"
 
 "@babel/runtime@^7.0.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
-  version "7.15.3"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b"
-  integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
+  integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
   dependencies:
     regenerator-runtime "^0.13.4"
 
-"@babel/template@^7.14.5", "@babel/template@^7.3.3":
-  version "7.14.5"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4"
-  integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==
+"@babel/template@^7.15.4", "@babel/template@^7.3.3":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.15.4.tgz#51898d35dcf3faa670c4ee6afcfd517ee139f194"
+  integrity sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==
   dependencies:
     "@babel/code-frame" "^7.14.5"
-    "@babel/parser" "^7.14.5"
-    "@babel/types" "^7.14.5"
+    "@babel/parser" "^7.15.4"
+    "@babel/types" "^7.15.4"
 
-"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.12", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.17", "@babel/traverse@^7.14.5", "@babel/traverse@^7.15.0":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.0.tgz#4cca838fd1b2a03283c1f38e141f639d60b3fc98"
-  integrity sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.12", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.17", "@babel/traverse@^7.15.4":
+  version "7.15.4"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.15.4.tgz#ff8510367a144bfbff552d9e18e28f3e2889c22d"
+  integrity sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==
   dependencies:
     "@babel/code-frame" "^7.14.5"
-    "@babel/generator" "^7.15.0"
-    "@babel/helper-function-name" "^7.14.5"
-    "@babel/helper-hoist-variables" "^7.14.5"
-    "@babel/helper-split-export-declaration" "^7.14.5"
-    "@babel/parser" "^7.15.0"
-    "@babel/types" "^7.15.0"
+    "@babel/generator" "^7.15.4"
+    "@babel/helper-function-name" "^7.15.4"
+    "@babel/helper-hoist-variables" "^7.15.4"
+    "@babel/helper-split-export-declaration" "^7.15.4"
+    "@babel/parser" "^7.15.4"
+    "@babel/types" "^7.15.4"
     debug "^4.1.0"
     globals "^11.1.0"
 
-"@babel/types@^7.0.0", "@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.14.9", "@babel/types@^7.15.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
-  version "7.15.0"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.0.tgz#61af11f2286c4e9c69ca8deb5f4375a73c72dcbd"
-  integrity sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==
+"@babel/types@^7.0.0", "@babel/types@^7.14.9", "@babel/types@^7.15.4", "@babel/types@^7.15.6", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
+  version "7.15.6"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.15.6.tgz#99abdc48218b2881c058dd0a7ab05b99c9be758f"
+  integrity sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==
   dependencies:
     "@babel/helper-validator-identifier" "^7.14.9"
     to-fast-properties "^2.0.0"
@@ -1353,7 +1353,7 @@
   dependencies:
     "@octokit/types" "^6.0.3"
 
-"@octokit/core@^3.4.0", "@octokit/core@^3.5.0":
+"@octokit/core@^3.4.0", "@octokit/core@^3.5.1":
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b"
   integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==
@@ -1376,37 +1376,37 @@
     universal-user-agent "^6.0.0"
 
 "@octokit/graphql@^4.5.8":
-  version "4.6.4"
-  resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.6.4.tgz#0c3f5bed440822182e972317122acb65d311a5ed"
-  integrity sha512-SWTdXsVheRmlotWNjKzPOb6Js6tjSqA2a8z9+glDJng0Aqjzti8MEWOtuT8ZSu6wHnci7LZNuarE87+WJBG4vg==
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3"
+  integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==
   dependencies:
     "@octokit/request" "^5.6.0"
     "@octokit/types" "^6.0.3"
     universal-user-agent "^6.0.0"
 
-"@octokit/openapi-types@^9.5.0":
-  version "9.7.0"
-  resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-9.7.0.tgz#9897cdefd629cd88af67b8dbe2e5fb19c63426b2"
-  integrity sha512-TUJ16DJU8mekne6+KVcMV5g6g/rJlrnIKn7aALG9QrNpnEipFc1xjoarh0PKaAWf2Hf+HwthRKYt+9mCm5RsRg==
+"@octokit/openapi-types@^10.2.2":
+  version "10.2.2"
+  resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-10.2.2.tgz#6c1c839d7d169feabaf1d2a69c79439c75d979cd"
+  integrity sha512-EVcXQ+ZrC04cg17AMg1ofocWMxHDn17cB66ZHgYc0eUwjFtxS0oBzkyw2VqIrHBwVgtfoYrq1WMQfQmMjUwthw==
 
-"@octokit/plugin-paginate-rest@^2.13.3", "@octokit/plugin-paginate-rest@^2.6.2":
-  version "2.15.1"
-  resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.15.1.tgz#264189dd3ce881c6c33758824aac05a4002e056a"
-  integrity sha512-47r52KkhQDkmvUKZqXzA1lKvcyJEfYh3TKAIe5+EzMeyDM3d+/s5v11i2gTk8/n6No6DPi3k5Ind6wtDbo/AEg==
+"@octokit/plugin-paginate-rest@^2.13.3", "@octokit/plugin-paginate-rest@^2.16.0":
+  version "2.16.3"
+  resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.16.3.tgz#6dbf74a12a53e04da6ca731d4c93f20c0b5c6fe9"
+  integrity sha512-kdc65UEsqze/9fCISq6BxLzeB9qf0vKvKojIfzgwf4tEF+Wy6c9dXnPFE6vgpoDFB1Z5Jek5WFVU6vL1w22+Iw==
   dependencies:
-    "@octokit/types" "^6.24.0"
+    "@octokit/types" "^6.28.1"
 
-"@octokit/plugin-request-log@^1.0.2":
+"@octokit/plugin-request-log@^1.0.4":
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85"
   integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==
 
-"@octokit/plugin-rest-endpoint-methods@5.8.0", "@octokit/plugin-rest-endpoint-methods@^5.1.1":
-  version "5.8.0"
-  resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.8.0.tgz#33b342fe41f2603fdf8b958e6652103bb3ea3f3b"
-  integrity sha512-qeLZZLotNkoq+it6F+xahydkkbnvSK0iDjlXFo3jNTB+Ss0qIbYQb9V/soKLMkgGw8Q2sHjY5YEXiA47IVPp4A==
+"@octokit/plugin-rest-endpoint-methods@^5.1.1", "@octokit/plugin-rest-endpoint-methods@^5.9.0":
+  version "5.10.4"
+  resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.10.4.tgz#97e85eb7375e30b9bf193894670f9da205e79408"
+  integrity sha512-Dh+EAMCYR9RUHwQChH94Skl0lM8Fh99auT8ggck/xTzjJrwVzvsd0YH68oRPqp/HxICzmUjLfaQ9sy1o1sfIiA==
   dependencies:
-    "@octokit/types" "^6.25.0"
+    "@octokit/types" "^6.28.1"
     deprecation "^2.3.1"
 
 "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0":
@@ -1431,23 +1431,23 @@
     universal-user-agent "^6.0.0"
 
 "@octokit/rest@^18.6.7":
-  version "18.9.1"
-  resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.9.1.tgz#db1d7ac1d7b10e908f7d4b78fe35a392554ccb26"
-  integrity sha512-idZ3e5PqXVWOhtZYUa546IDHTHjkGZbj3tcJsN0uhCy984KD865e8GB2WbYDc2ZxFuJRiyd0AftpL2uPNhF+UA==
+  version "18.10.0"
+  resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.10.0.tgz#8a0add9611253e0e31d3ed5b4bc941a3795a7648"
+  integrity sha512-esHR5OKy38bccL/sajHqZudZCvmv4yjovMJzyXlphaUo7xykmtOdILGJ3aAm0mFHmMLmPFmDMJXf39cAjNJsrw==
   dependencies:
-    "@octokit/core" "^3.5.0"
-    "@octokit/plugin-paginate-rest" "^2.6.2"
-    "@octokit/plugin-request-log" "^1.0.2"
-    "@octokit/plugin-rest-endpoint-methods" "5.8.0"
+    "@octokit/core" "^3.5.1"
+    "@octokit/plugin-paginate-rest" "^2.16.0"
+    "@octokit/plugin-request-log" "^1.0.4"
+    "@octokit/plugin-rest-endpoint-methods" "^5.9.0"
 
-"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.24.0", "@octokit/types@^6.25.0":
-  version "6.25.0"
-  resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.25.0.tgz#c8e37e69dbe7ce55ed98ee63f75054e7e808bf1a"
-  integrity sha512-bNvyQKfngvAd/08COlYIN54nRgxskmejgywodizQNyiKoXmWRAjKup2/LYwm+T9V0gsKH6tuld1gM0PzmOiB4Q==
+"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.28.1":
+  version "6.28.1"
+  resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.28.1.tgz#ab990d1fe952226055e81c7650480e6bacfb877c"
+  integrity sha512-XlxDoQLFO5JnFZgKVQTYTvXRsQFfr/GwDUU108NJ9R5yFPkA2qXhTJjYuul3vE4eLXP40FA2nysOu2zd6boE+w==
   dependencies:
-    "@octokit/openapi-types" "^9.5.0"
+    "@octokit/openapi-types" "^10.2.2"
 
-"@peculiar/asn1-schema@^2.0.27", "@peculiar/asn1-schema@^2.0.32":
+"@peculiar/asn1-schema@^2.0.32", "@peculiar/asn1-schema@^2.0.38":
   version "2.0.38"
   resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.38.tgz#98b6f12daad275ecd6774dfe31fb62f362900412"
   integrity sha512-zZ64UpCTm9me15nuCpPgJghSdbEm8atcDQPCyK+bKXjZAQ1735NCZXCSCfbckbQ4MH36Rm9403n/qMq77LFDzQ==
@@ -1476,66 +1476,66 @@
     webcrypto-core "^1.2.0"
 
 "@sentry/browser@^6.11.0":
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.11.0.tgz#9e90bbc0488ebcdd1e67937d8d5b4f13c3f6dee0"
-  integrity sha512-Qr2QRA0t5/S9QQqxzYKvM9W8prvmiWuldfwRX4hubovXzcXLgUi4WK0/H612wSbYZ4dNAEcQbtlxFWJNN4wxdg==
+  version "6.12.0"
+  resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.12.0.tgz#970cd68fa117a1e1336fdb373e3b1fa76cd63e2d"
+  integrity sha512-wsJi1NLOmfwtPNYxEC50dpDcVY7sdYckzwfqz1/zHrede1mtxpqSw+7iP4bHADOJXuF+ObYYTHND0v38GSXznQ==
   dependencies:
-    "@sentry/core" "6.11.0"
-    "@sentry/types" "6.11.0"
-    "@sentry/utils" "6.11.0"
+    "@sentry/core" "6.12.0"
+    "@sentry/types" "6.12.0"
+    "@sentry/utils" "6.12.0"
     tslib "^1.9.3"
 
-"@sentry/core@6.11.0":
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.11.0.tgz#40e94043afcf6407a109be26655c77832c64e740"
-  integrity sha512-09TB+f3pqEq8LFahFWHO6I/4DxHo+NcS52OkbWMDqEi6oNZRD7PhPn3i14LfjsYVv3u3AESU8oxSEGbFrr2UjQ==
+"@sentry/core@6.12.0":
+  version "6.12.0"
+  resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.12.0.tgz#bc7c5f0785b6a392d9ad47bd9b1fae3f5389996c"
+  integrity sha512-mU/zdjlzFHzdXDZCPZm8OeCw7c9xsbL49Mq0TrY0KJjLt4CJBkiq5SDTGfRsenBLgTedYhe5Z/J8Z+xVVq+MfQ==
   dependencies:
-    "@sentry/hub" "6.11.0"
-    "@sentry/minimal" "6.11.0"
-    "@sentry/types" "6.11.0"
-    "@sentry/utils" "6.11.0"
+    "@sentry/hub" "6.12.0"
+    "@sentry/minimal" "6.12.0"
+    "@sentry/types" "6.12.0"
+    "@sentry/utils" "6.12.0"
     tslib "^1.9.3"
 
-"@sentry/hub@6.11.0":
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.11.0.tgz#ddf9ddb0577d1c8290dc02c0242d274fe84d6c16"
-  integrity sha512-pT9hf+ZJfVFpoZopoC+yJmFNclr4NPqPcl2cgguqCHb69DklD1NxgBNWK8D6X05qjnNFDF991U6t1mxP9HrGuw==
+"@sentry/hub@6.12.0":
+  version "6.12.0"
+  resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.12.0.tgz#29e323ab6a95e178fb14fffb684aa0e09707197f"
+  integrity sha512-yR/UQVU+ukr42bSYpeqvb989SowIXlKBanU0cqLFDmv5LPCnaQB8PGeXwJAwWhQgx44PARhmB82S6Xor8gYNxg==
   dependencies:
-    "@sentry/types" "6.11.0"
-    "@sentry/utils" "6.11.0"
+    "@sentry/types" "6.12.0"
+    "@sentry/utils" "6.12.0"
     tslib "^1.9.3"
 
-"@sentry/minimal@6.11.0":
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.11.0.tgz#806d5512658370e40827b3e3663061db708fff33"
-  integrity sha512-XkZ7qrdlGp4IM/gjGxf1Q575yIbl5RvPbg+WFeekpo16Ufvzx37Mr8c2xsZaWosISVyE6eyFpooORjUlzy8EDw==
+"@sentry/minimal@6.12.0":
+  version "6.12.0"
+  resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.12.0.tgz#cbe20e95056cedb9709d7d5b2119ef95206a9f8c"
+  integrity sha512-r3C54Q1KN+xIqUvcgX9DlcoWE7ezWvFk2pSu1Ojx9De81hVqR9u5T3sdSAP2Xma+um0zr6coOtDJG4WtYlOtsw==
   dependencies:
-    "@sentry/hub" "6.11.0"
-    "@sentry/types" "6.11.0"
+    "@sentry/hub" "6.12.0"
+    "@sentry/types" "6.12.0"
     tslib "^1.9.3"
 
 "@sentry/tracing@^6.11.0":
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.11.0.tgz#9bd9287addea1ebc12c75b226f71c7713c0fac4f"
-  integrity sha512-9VA1/SY++WeoMQI4K6n/sYgIdRtCu9NLWqmGqu/5kbOtESYFgAt1DqSyqGCr00ZjQiC2s7tkDkTNZb38K6KytQ==
+  version "6.12.0"
+  resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.12.0.tgz#a05c8985ee7fed7310b029b147d8f9f14f2a2e67"
+  integrity sha512-u10QHNknPBzbWSUUNMkvuH53sQd5NaBo6YdNPj4p5b7sE7445Sh0PwBpRbY3ZiUUiwyxV59fx9UQ4yVnPGxZQA==
   dependencies:
-    "@sentry/hub" "6.11.0"
-    "@sentry/minimal" "6.11.0"
-    "@sentry/types" "6.11.0"
-    "@sentry/utils" "6.11.0"
+    "@sentry/hub" "6.12.0"
+    "@sentry/minimal" "6.12.0"
+    "@sentry/types" "6.12.0"
+    "@sentry/utils" "6.12.0"
     tslib "^1.9.3"
 
-"@sentry/types@6.11.0", "@sentry/types@^6.10.0":
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.11.0.tgz#5122685478d32ddacd3a891cbcf550012df85f7c"
-  integrity sha512-gm5H9eZhL6bsIy/h3T+/Fzzz2vINhHhqd92CjHle3w7uXdTdFV98i2pDpErBGNTSNzbntqOMifYEB5ENtZAvcg==
+"@sentry/types@6.12.0", "@sentry/types@^6.10.0":
+  version "6.12.0"
+  resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.12.0.tgz#b7395688a79403c6df8d8bb8d81deb8222519853"
+  integrity sha512-urtgLzE4EDMAYQHYdkgC0Ei9QvLajodK1ntg71bGn0Pm84QUpaqpPDfHRU+i6jLeteyC7kWwa5O5W1m/jrjGXA==
 
-"@sentry/utils@6.11.0":
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.11.0.tgz#d1dee4faf4d9c42c54bba88d5a66fb96b902a14c"
-  integrity sha512-IOvyFHcnbRQxa++jO+ZUzRvFHEJ1cZjrBIQaNVc0IYF0twUOB5PTP6joTcix38ldaLeapaPZ9LGfudbvYvxkdg==
+"@sentry/utils@6.12.0":
+  version "6.12.0"
+  resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.12.0.tgz#3de261e8d11bdfdc7add64a3065d43517802e975"
+  integrity sha512-oRHQ7TH5TSsJqoP9Gqq25Jvn9LKexXfAh/OoKwjMhYCGKGhqpDNUIZVgl9DWsGw5A5N5xnQyLOxDfyRV5RshdA==
   dependencies:
-    "@sentry/types" "6.11.0"
+    "@sentry/types" "6.12.0"
     tslib "^1.9.3"
 
 "@sinonjs/commons@^1.7.0":
@@ -1585,9 +1585,9 @@
   integrity sha512-t4YHCgtD+ERvH0FyxvNlYwJ2ezhqw7t+Ygh4urQ7dJER8i185JPv6oIM3ey5YQmGN6Zp9EMbpohkjZi9t3UxwA==
 
 "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
-  version "7.1.15"
-  resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024"
-  integrity sha512-bxlMKPDbY8x5h6HBwVzEOk2C8fb6SLfYQ5Jw3uBYuYF1lfWk/kbLd81la82vrIkBb0l+JdmrZaDikPrNxpS/Ew==
+  version "7.1.16"
+  resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.16.tgz#bc12c74b7d65e82d29876b5d0baf5c625ac58702"
+  integrity sha512-EAEHtisTMM+KaKwfWdC3oyllIqswlznXCIVCt7/oRNrh+DhgT4UEBNC/jlADNjvw7UnfbcdkGQcPVZ1xYiLcrQ==
   dependencies:
     "@babel/parser" "^7.1.0"
     "@babel/types" "^7.0.0"
@@ -1747,14 +1747,14 @@
   integrity sha512-jhMOZSS0UGYTS9pqvt6q3wtT3uvOSve5piTEmTMx3zzTuBLvSIMxSIBIc3d5lajVD5h4xc41AMZD2M5orN3PxA==
 
 "@types/node@*":
-  version "16.7.1"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.1.tgz#c6b9198178da504dfca1fd0be9b2e1002f1586f0"
-  integrity sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A==
+  version "16.9.1"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
+  integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
 
 "@types/node@^14.14.22":
-  version "14.17.11"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.11.tgz#82d266d657aec5ff01ca59f2ffaff1bb43f7bf0f"
-  integrity sha512-n2OQ+0Bz6WEsUjrvcHD1xZ8K+Kgo4cn9/w94s1bJS690QMUWfJPW/m7CCb7gPkA1fcYwL2UpjXP/rq/Eo41m6w==
+  version "14.17.16"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.16.tgz#2b9252bd4fdf0393696190cd9550901dd967c777"
+  integrity sha512-WiFf2izl01P1CpeY8WqFAeKWwByMueBEkND38EcN8N68qb0aDG3oIS1P5MhAX5kUdr469qRyqsY/MjanLjsFbQ==
 
 "@types/normalize-package-data@^2.4.0":
   version "2.4.1"
@@ -1794,13 +1794,13 @@
     "@types/node" "*"
 
 "@types/react-beautiful-dnd@^13.0.0":
-  version "13.1.1"
-  resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#fb3fe24a334cc757d290e75722e4d3c8368ce3a3"
-  integrity sha512-1lBBxVSutE8CQM37Jq7KvJwuA94qaEEqsx+G0dnwzG6Sfwf6JGcNeFk5jjjhJli1q2naeMZm+D/dvT/zyX4QPw==
+  version "13.1.2"
+  resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130"
+  integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==
   dependencies:
     "@types/react" "*"
 
-"@types/react-dom@^17.0.2":
+"@types/react-dom@17.0.9":
   version "17.0.9"
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
   integrity sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==
@@ -1824,7 +1824,7 @@
   dependencies:
     "@types/react" "*"
 
-"@types/react@*", "@types/react@^17.0.2":
+"@types/react@*", "@types/react@17.0.14":
   version "17.0.14"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.14.tgz#f0629761ca02945c4e8fea99b8177f4c5c61fb0f"
   integrity sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==
@@ -1883,72 +1883,72 @@
   integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==
 
 "@typescript-eslint/eslint-plugin@^4.17.0":
-  version "4.29.3"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.3.tgz#95cb8029a8bd8bd9c7f4ab95074a7cb2115adefa"
-  integrity sha512-tBgfA3K/3TsZY46ROGvoRxQr1wBkclbVqRQep97MjVHJzcRBURRY3sNFqLk0/Xr//BY5hM9H2p/kp+6qim85SA==
+  version "4.31.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.1.tgz#e938603a136f01dcabeece069da5fb2e331d4498"
+  integrity sha512-UDqhWmd5i0TvPLmbK5xY3UZB0zEGseF+DHPghZ37Sb83Qd3p8ujhvAtkU4OF46Ka5Pm5kWvFIx0cCTBFKo0alA==
   dependencies:
-    "@typescript-eslint/experimental-utils" "4.29.3"
-    "@typescript-eslint/scope-manager" "4.29.3"
+    "@typescript-eslint/experimental-utils" "4.31.1"
+    "@typescript-eslint/scope-manager" "4.31.1"
     debug "^4.3.1"
     functional-red-black-tree "^1.0.1"
     regexpp "^3.1.0"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/experimental-utils@4.29.3":
-  version "4.29.3"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.3.tgz#52e437a689ccdef73e83c5106b34240a706f15e1"
-  integrity sha512-ffIvbytTVWz+3keg+Sy94FG1QeOvmV9dP2YSdLFHw/ieLXWCa3U1TYu8IRCOpMv2/SPS8XqhM1+ou1YHsdzKrg==
+"@typescript-eslint/experimental-utils@4.31.1":
+  version "4.31.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.1.tgz#0c900f832f270b88e13e51753647b02d08371ce5"
+  integrity sha512-NtoPsqmcSsWty0mcL5nTZXMf7Ei0Xr2MT8jWjXMVgRK0/1qeQ2jZzLFUh4QtyJ4+/lPUyMw5cSfeeME+Zrtp9Q==
   dependencies:
     "@types/json-schema" "^7.0.7"
-    "@typescript-eslint/scope-manager" "4.29.3"
-    "@typescript-eslint/types" "4.29.3"
-    "@typescript-eslint/typescript-estree" "4.29.3"
+    "@typescript-eslint/scope-manager" "4.31.1"
+    "@typescript-eslint/types" "4.31.1"
+    "@typescript-eslint/typescript-estree" "4.31.1"
     eslint-scope "^5.1.1"
     eslint-utils "^3.0.0"
 
 "@typescript-eslint/parser@^4.17.0":
-  version "4.29.3"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.3.tgz#2ac25535f34c0e98f50c0e6b28c679c2357d45f2"
-  integrity sha512-jrHOV5g2u8ROghmspKoW7pN8T/qUzk0+DITun0MELptvngtMrwUJ1tv5zMI04CYVEUsSrN4jV7AKSv+I0y0EfQ==
+  version "4.31.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.31.1.tgz#8f9a2672033e6f6d33b1c0260eebdc0ddf539064"
+  integrity sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ==
   dependencies:
-    "@typescript-eslint/scope-manager" "4.29.3"
-    "@typescript-eslint/types" "4.29.3"
-    "@typescript-eslint/typescript-estree" "4.29.3"
+    "@typescript-eslint/scope-manager" "4.31.1"
+    "@typescript-eslint/types" "4.31.1"
+    "@typescript-eslint/typescript-estree" "4.31.1"
     debug "^4.3.1"
 
-"@typescript-eslint/scope-manager@4.29.3":
-  version "4.29.3"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.3.tgz#497dec66f3a22e459f6e306cf14021e40ec86e19"
-  integrity sha512-x+w8BLXO7iWPkG5mEy9bA1iFRnk36p/goVlYobVWHyDw69YmaH9q6eA+Fgl7kYHmFvWlebUTUfhtIg4zbbl8PA==
+"@typescript-eslint/scope-manager@4.31.1":
+  version "4.31.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.31.1.tgz#0c21e8501f608d6a25c842fcf59541ef4f1ab561"
+  integrity sha512-N1Uhn6SqNtU2XpFSkD4oA+F0PfKdWHyr4bTX0xTj8NRx1314gBDRL1LUuZd5+L3oP+wo6hCbZpaa1in6SwMcVQ==
   dependencies:
-    "@typescript-eslint/types" "4.29.3"
-    "@typescript-eslint/visitor-keys" "4.29.3"
+    "@typescript-eslint/types" "4.31.1"
+    "@typescript-eslint/visitor-keys" "4.31.1"
 
-"@typescript-eslint/types@4.29.3":
-  version "4.29.3"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.3.tgz#d7980c49aef643d0af8954c9f14f656b7fd16017"
-  integrity sha512-s1eV1lKNgoIYLAl1JUba8NhULmf+jOmmeFO1G5MN/RBCyyzg4TIOfIOICVNC06lor+Xmy4FypIIhFiJXOknhIg==
+"@typescript-eslint/types@4.31.1":
+  version "4.31.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.31.1.tgz#5f255b695627a13401d2fdba5f7138bc79450d66"
+  integrity sha512-kixltt51ZJGKENNW88IY5MYqTBA8FR0Md8QdGbJD2pKZ+D5IvxjTYDNtJPDxFBiXmka2aJsITdB1BtO1fsgmsQ==
 
-"@typescript-eslint/typescript-estree@4.29.3":
-  version "4.29.3"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.3.tgz#1bafad610015c4ded35c85a70b6222faad598b40"
-  integrity sha512-45oQJA0bxna4O5TMwz55/TpgjX1YrAPOI/rb6kPgmdnemRZx/dB0rsx+Ku8jpDvqTxcE1C/qEbVHbS3h0hflag==
+"@typescript-eslint/typescript-estree@4.31.1":
+  version "4.31.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.1.tgz#4a04d5232cf1031232b7124a9c0310b577a62d17"
+  integrity sha512-EGHkbsUvjFrvRnusk6yFGqrqMBTue5E5ROnS5puj3laGQPasVUgwhrxfcgkdHNFECHAewpvELE1Gjv0XO3mdWg==
   dependencies:
-    "@typescript-eslint/types" "4.29.3"
-    "@typescript-eslint/visitor-keys" "4.29.3"
+    "@typescript-eslint/types" "4.31.1"
+    "@typescript-eslint/visitor-keys" "4.31.1"
     debug "^4.3.1"
     globby "^11.0.3"
     is-glob "^4.0.1"
     semver "^7.3.5"
     tsutils "^3.21.0"
 
-"@typescript-eslint/visitor-keys@4.29.3":
-  version "4.29.3"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.3.tgz#c691760a00bd86bf8320d2a90a93d86d322f1abf"
-  integrity sha512-MGGfJvXT4asUTeVs0Q2m+sY63UsfnA+C/FDgBKV3itLBmM9H0u+URcneePtkd0at1YELmZK6HSolCqM4Fzs6yA==
+"@typescript-eslint/visitor-keys@4.31.1":
+  version "4.31.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.1.tgz#f2e7a14c7f20c4ae07d7fc3c5878c4441a1da9cc"
+  integrity sha512-PCncP8hEqKw6SOJY+3St4LVtoZpPPn+Zlpm7KW5xnviMhdqcsBty4Lsg4J/VECpJjw1CkROaZhH4B8M1OfnXTQ==
   dependencies:
-    "@typescript-eslint/types" "4.29.3"
+    "@typescript-eslint/types" "4.31.1"
     eslint-visitor-keys "^2.0.0"
 
 "@wojtekmaj/enzyme-adapter-react-17@^0.6.1":
@@ -2005,9 +2005,9 @@ acorn@^7.1.1, acorn@^7.4.0:
   integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
 
 acorn@^8.2.4:
-  version "8.4.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
-  integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
+  version "8.5.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2"
+  integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==
 
 agent-base@6:
   version "6.0.2"
@@ -2027,9 +2027,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
     uri-js "^4.2.2"
 
 ajv@^8.0.1:
-  version "8.6.2"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571"
-  integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==
+  version "8.6.3"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764"
+  integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==
   dependencies:
     fast-deep-equal "^3.1.1"
     json-schema-traverse "^1.0.0"
@@ -2078,9 +2078,9 @@ ansi-regex@^4.1.0:
   integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
 
 ansi-regex@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
-  integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
 
 ansi-styles@^3.2.0, ansi-styles@^3.2.1:
   version "3.2.1"
@@ -2207,7 +2207,7 @@ asn1@~0.2.3:
   dependencies:
     safer-buffer "~2.1.0"
 
-asn1js@^2.0.26, asn1js@^2.1.1:
+asn1js@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-2.1.1.tgz#bb3896191ebb5fb1caeda73436a6c6e20a2eedff"
   integrity sha512-t9u0dU0rJN4ML+uxgN6VM2Z4H5jWIYm0w8LsZLzMJaQsgL3IJNbxHgmbWDvJAwspyHpDFuzUaUFh4c05UB4+6g==
@@ -2257,10 +2257,10 @@ autoprefixer@^9.8.6:
     postcss "^7.0.32"
     postcss-value-parser "^4.1.0"
 
-available-typed-arrays@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz#9e0ae84ecff20caae6a94a1c3bc39b955649b7a9"
-  integrity sha512-SA5mXJWrId1TaQjfxUYghbqQ/hYioKmLJvPJyDuYRtXXenFNMjj4hSSt1Cf1xsuXSXrtxrVC5Ot4eU6cOtBDdA==
+available-typed-arrays@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
+  integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
 
 await-lock@^2.1.0:
   version "2.1.0"
@@ -2492,14 +2492,14 @@ browser-request@^0.3.3:
   resolved "https://registry.yarnpkg.com/browser-request/-/browser-request-0.3.3.tgz#9ece5b5aca89a29932242e18bf933def9876cc17"
   integrity sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=
 
-browserslist@^4.12.0, browserslist@^4.16.6, browserslist@^4.16.8:
-  version "4.16.8"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0"
-  integrity sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==
+browserslist@^4.12.0, browserslist@^4.16.6, browserslist@^4.17.0:
+  version "4.17.0"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.0.tgz#1fcd81ec75b41d6d4994fb0831b92ac18c01649c"
+  integrity sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g==
   dependencies:
-    caniuse-lite "^1.0.30001251"
+    caniuse-lite "^1.0.30001254"
     colorette "^1.3.0"
-    electron-to-chromium "^1.3.811"
+    electron-to-chromium "^1.3.830"
     escalade "^3.1.1"
     node-releases "^1.1.75"
 
@@ -2595,10 +2595,10 @@ camelcase@^6.0.0:
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
   integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
 
-caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001251:
-  version "1.0.30001251"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz#6853a606ec50893115db660f82c094d18f096d85"
-  integrity sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==
+caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001254:
+  version "1.0.30001257"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz#150aaf649a48bee531104cfeda57f92ce587f6e5"
+  integrity sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==
 
 capture-exit@^2.0.0:
   version "2.0.0"
@@ -2819,9 +2819,9 @@ color-name@^1.1.4, color-name@~1.1.4:
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
 colorette@^1.2.1, colorette@^1.2.2, colorette@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af"
-  integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
+  integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
 
 combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
   version "1.0.8"
@@ -2898,11 +2898,11 @@ copy-descriptor@^0.1.0:
   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
 
 core-js-compat@^3.14.0, core-js-compat@^3.16.0:
-  version "3.16.3"
-  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.16.3.tgz#ae12a6e20505a1d79fbd16b6689dfc77fc989114"
-  integrity sha512-A/OtSfSJQKLAFRVd4V0m6Sep9lPdjD8bpN8v3tCCGwE0Tmh0hOiVDm9tw6mXmWOKOSZIyr3EkywPo84cJjGvIQ==
+  version "3.17.3"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.17.3.tgz#b39c8e4dec71ecdc735c653ce5233466e561324e"
+  integrity sha512-+in61CKYs4hQERiADCJsdgewpdl/X0GhEX77pjKgbeibXviIt2oxEjTc8O2fqHX8mDdBrDvX8MYD/RYsBv4OiA==
   dependencies:
-    browserslist "^4.16.8"
+    browserslist "^4.17.0"
     semver "7.0.0"
 
 core-js@^1.0.0:
@@ -2910,11 +2910,16 @@ core-js@^1.0.0:
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
   integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
 
-core-util-is@1.0.2, core-util-is@~1.0.0:
+core-util-is@1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
   integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
 
+core-util-is@~1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
 cosmiconfig@^7.0.0:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d"
@@ -3020,9 +3025,9 @@ cssstyle@^2.3.0:
     cssom "~0.3.6"
 
 csstype@^3.0.2:
-  version "3.0.8"
-  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
-  integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
+  version "3.0.9"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b"
+  integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==
 
 d@1, d@^1.0.1:
   version "1.0.1"
@@ -3096,9 +3101,9 @@ decode-uri-component@^0.2.0:
   integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
 
 deep-is@^0.1.3, deep-is@~0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
-  integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
+  integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
 
 deepmerge@^4.2.2:
   version "4.2.2"
@@ -3250,9 +3255,9 @@ domhandler@^2.3.0:
     domelementtype "1"
 
 domhandler@^4.0.0, domhandler@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059"
-  integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
+  integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==
   dependencies:
     domelementtype "^2.2.0"
 
@@ -3265,9 +3270,9 @@ domutils@^1.5.1:
     domelementtype "1"
 
 domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442"
-  integrity sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
   dependencies:
     dom-serializer "^1.0.1"
     domelementtype "^2.2.0"
@@ -3281,10 +3286,10 @@ ecc-jsbn@~0.1.1:
     jsbn "~0.1.0"
     safer-buffer "^2.1.0"
 
-electron-to-chromium@^1.3.811:
-  version "1.3.817"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.817.tgz#911b4775b5d9fa0c4729d4694adc81de85d8d8f6"
-  integrity sha512-Vw0Faepf2Id9Kf2e97M/c99qf168xg86JLKDxivvlpBQ9KDtjSeX0v+TiuSE25PqeQfTz+NJs375b64ca3XOIQ==
+electron-to-chromium@^1.3.830:
+  version "1.3.839"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.839.tgz#27a5b21468e9fefb0e328a029403617f20acec9c"
+  integrity sha512-0O7uPs9LJNjQ/U5mW78qW8gXv9H6Ba3DHZ5/yt8aBsvomOWDkV3MddT7enUYvLQEUVOURjWmgJJWVZ3K98tIwQ==
 
 emittery@^0.7.1:
   version "0.7.2"
@@ -3390,22 +3395,23 @@ error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.18.0, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2, es-abstract@^1.18.5:
-  version "1.18.5"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.5.tgz#9b10de7d4c206a3581fd5b2124233e04db49ae19"
-  integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==
+es-abstract@^1.18.0, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2, es-abstract@^1.18.5, es-abstract@^1.18.6:
+  version "1.18.6"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.6.tgz#2c44e3ea7a6255039164d26559777a6d978cb456"
+  integrity sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ==
   dependencies:
     call-bind "^1.0.2"
     es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
     get-intrinsic "^1.1.1"
+    get-symbol-description "^1.0.0"
     has "^1.0.3"
     has-symbols "^1.0.2"
     internal-slot "^1.0.3"
-    is-callable "^1.2.3"
+    is-callable "^1.2.4"
     is-negative-zero "^2.0.1"
-    is-regex "^1.1.3"
-    is-string "^1.0.6"
+    is-regex "^1.1.4"
+    is-string "^1.0.7"
     object-inspect "^1.11.0"
     object-keys "^1.1.1"
     object.assign "^4.1.2"
@@ -3529,13 +3535,14 @@ eslint-plugin-react-hooks@^4.2.0:
   integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==
 
 eslint-plugin-react@^7.22.0:
-  version "7.24.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.24.0.tgz#eadedfa351a6f36b490aa17f4fa9b14e842b9eb4"
-  integrity sha512-KJJIx2SYx7PBx3ONe/mEeMz4YE0Lcr7feJTCMyyKb/341NcjuAgim3Acgan89GfPv7nxXK2+0slu0CWXYM4x+Q==
+  version "7.25.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz#9286b7cd9bf917d40309760f403e53016eda8331"
+  integrity sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug==
   dependencies:
     array-includes "^3.1.3"
     array.prototype.flatmap "^1.2.4"
     doctrine "^2.1.0"
+    estraverse "^5.2.0"
     has "^1.0.3"
     jsx-ast-utils "^2.4.1 || ^3.0.0"
     minimatch "^3.0.4"
@@ -3860,9 +3867,9 @@ fastest-levenshtein@^1.0.12:
   integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
 
 fastq@^1.6.0:
-  version "1.12.0"
-  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.12.0.tgz#ed7b6ab5d62393fb2cc591c853652a5c318bf794"
-  integrity sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
+  integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
   dependencies:
     reusify "^1.0.4"
 
@@ -3988,9 +3995,9 @@ flux@2.1.1:
     immutable "^3.7.4"
 
 focus-lock@^0.9.1:
-  version "0.9.1"
-  resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.9.1.tgz#e8ec7d4821631112193ae09258107f531588da01"
-  integrity sha512-/2Nj60Cps6yOLSO+CkVbeSKfwfns5XbX6HOedIK9PdzODP04N9c3xqOcPXayN0WsT9YjJvAnXmI0NdqNIDf5Kw==
+  version "0.9.2"
+  resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.9.2.tgz#9d30918aaa99b1b97677731053d017f82a540d5b"
+  integrity sha512-YtHxjX7a0IC0ZACL5wsX8QdncXofWpGPNoVMuI/nZUrPGp6LmNI6+D5j0pPj+v8Kw5EpweA+T5yImK0rnWf7oQ==
   dependencies:
     tslib "^2.0.3"
 
@@ -4127,6 +4134,14 @@ get-stream@^5.0.0:
   dependencies:
     pump "^3.0.0"
 
+get-symbol-description@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+  integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+  dependencies:
+    call-bind "^1.0.2"
+    get-intrinsic "^1.1.1"
+
 get-value@^2.0.3, get-value@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -4613,7 +4628,7 @@ is-buffer@^2.0.0:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
   integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
 
-is-callable@^1.0.4, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.3, is-callable@^1.2.4:
+is-callable@^1.0.4, is-callable@^1.1.4, is-callable@^1.1.5, is-callable@^1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
   integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
@@ -4832,7 +4847,7 @@ is-promise@^2.2.2:
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
   integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
 
-is-regex@^1.0.3, is-regex@^1.0.5, is-regex@^1.1.3, is-regex@^1.1.4:
+is-regex@^1.0.3, is-regex@^1.0.5, is-regex@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
   integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
@@ -4860,7 +4875,7 @@ is-stream@^2.0.0:
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
   integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
 
-is-string@^1.0.5, is-string@^1.0.6, is-string@^1.0.7:
+is-string@^1.0.5, is-string@^1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
   integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
@@ -4879,12 +4894,12 @@ is-symbol@^1.0.2, is-symbol@^1.0.3, is-symbol@^1.0.4:
   dependencies:
     has-symbols "^1.0.2"
 
-is-typed-array@^1.1.6:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.7.tgz#881ddc660b13cb8423b2090fa88c0fe37a83eb2f"
-  integrity sha512-VxlpTBGknhQ3o7YiVjIhdLU6+oD8dPz/79vvvH4F+S/c8608UCVa9fgDpa1kZgFoUST2DCgacc70UszKgzKuvA==
+is-typed-array@^1.1.7:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.8.tgz#cbaa6585dc7db43318bc5b89523ea384a6f65e79"
+  integrity sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==
   dependencies:
-    available-typed-arrays "^1.0.4"
+    available-typed-arrays "^1.0.5"
     call-bind "^1.0.2"
     es-abstract "^1.18.5"
     foreach "^2.0.5"
@@ -5792,8 +5807,8 @@ mathml-tag-names@^2.1.3:
   integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
 
 "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
-  version "12.4.0"
-  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2783d162b77d6629c574f35e88bea9ae29765c34"
+  version "12.5.0"
+  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f84905b00398072b592addfb1dae64c8f3a07fa2"
   dependencies:
     "@babel/runtime" "^7.12.5"
     another-json "^0.2.0"
@@ -6075,7 +6090,7 @@ nice-try@^1.0.4:
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
-node-fetch@2.6.1, node-fetch@^2.6.1:
+node-fetch@2.6.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
   integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@@ -6088,6 +6103,11 @@ node-fetch@^1.0.1:
     encoding "^0.1.11"
     is-stream "^1.0.1"
 
+node-fetch@^2.6.1:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.2.tgz#986996818b73785e47b1965cc34eb093a1d464d0"
+  integrity sha512-aLoxToI6RfZ+0NOjmWAgn9+LEd30YCkJKFSyWacNZdEKTit/ZMcKjGkTRo8uWEsnIb/hfKecNPEbln02PdWbcA==
+
 node-int64@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -6262,13 +6282,13 @@ object.fromentries@^2.0.0, object.fromentries@^2.0.4:
     has "^1.0.3"
 
 object.getprototypeof@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/object.getprototypeof/-/object.getprototypeof-1.0.1.tgz#dce7a9e6335b04db2e218afc5f423352a8ee3ada"
-  integrity sha512-orf7CoEkZKn1HYzA5KIt6G3Z2G4LKi1CiIK73c2PA2OK7ZASYp+rlIymYSs09qyrMm2o14U00z3VeD7MSsdvNw==
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/object.getprototypeof/-/object.getprototypeof-1.0.2.tgz#a993d88ca63d68f9c328186dd17d76d4188b3624"
+  integrity sha512-7KoF7BcUJi6YZ+7g9XqbaJzxsmp4jSjUw8TkOSdN/GF9ZAhDM/ssERDddC+WR556niTKtCk9HtwwsbnaEeWNlg==
   dependencies:
     call-bind "^1.0.2"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.1"
+    es-abstract "^1.18.6"
     reflect.getprototypeof "^1.0.0"
 
 object.pick@^1.3.0:
@@ -6707,7 +6727,7 @@ punycode@^2.1.0, punycode@^2.1.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
-pvtsutils@^1.1.2, pvtsutils@^1.1.6, pvtsutils@^1.2.0:
+pvtsutils@^1.1.6, pvtsutils@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.2.0.tgz#619e4767093d23cd600482600c16f4c36d3025bb"
   integrity sha512-IDefMJEQl7HX0FP2hIKJFnAR11klP1js2ixCrOaMhe3kXFK6RQ2ABUCuwWaaD4ib0hSbh2fGTICvWJJhDfNecA==
@@ -6785,9 +6805,9 @@ randexp@0.4.6:
     ret "~0.1.10"
 
 re-resizable@^6.9.0:
-  version "6.9.0"
-  resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.0.tgz#9c3059b389ced6ade602234cc5bb1e12d231cd47"
-  integrity sha512-3cUDG81ylyqI0Pdgle/RHwwRYq0ORZzsUaySOCO8IbEtNyaRtrIHYm/jMQ5pjcNiKCxR3vsSymIQZHwJq4gg2Q==
+  version "6.9.1"
+  resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.1.tgz#6be082b55d02364ca4bfee139e04feebdf52441c"
+  integrity sha512-KRYAgr9/j1PJ3K+t+MBhlQ+qkkoLDJ1rs0z1heIWvYbCW/9Vq4djDU+QumJ3hQbwwtzXF6OInla6rOx6hhgRhQ==
   dependencies:
     fast-memoize "^2.5.1"
 
@@ -6816,7 +6836,7 @@ react-clientside-effect@^1.2.5:
   dependencies:
     "@babel/runtime" "^7.12.13"
 
-react-dom@^17.0.2:
+react-dom@17.0.2:
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
   integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
@@ -6848,9 +6868,9 @@ react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
 react-redux@^7.2.0:
-  version "7.2.4"
-  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
-  integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
+  version "7.2.5"
+  resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.5.tgz#213c1b05aa1187d9c940ddfc0b29450957f6a3b8"
+  integrity sha512-Dt29bNyBsbQaysp6s/dN0gUodcq+dVKKER8Qv82UrpeygwYeX1raTtil7O/fftw/rFqzaf6gJhDZRkkZnn6bjg==
   dependencies:
     "@babel/runtime" "^7.12.1"
     "@types/react-redux" "^7.1.16"
@@ -6887,7 +6907,7 @@ react-transition-group@^4.4.1:
     loose-envify "^1.4.0"
     prop-types "^15.6.2"
 
-react@^17.0.2:
+react@17.0.2:
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
   integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
@@ -6977,24 +6997,24 @@ redux@^4.0.0, redux@^4.0.4:
     "@babel/runtime" "^7.9.2"
 
 reflect.getprototypeof@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.0.tgz#e54220a9bb67ec810c1916fc206ba6039509cb53"
-  integrity sha512-+0EPfQjXK+0X35YbfoXm6SKonJYwD1seJiS170Hl7MVLp5eGAKOGqbnLVtvC9boQ5qV5UpDNop+p0beVYbSI+Q==
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.1.tgz#f113f15e19d103d60b8d52ce7e0d45867efdddfc"
+  integrity sha512-z0FhUSaxXxnFQi+YyGfAvUJjCGPPwe0AO51LU/HtRLj/+a4LROrmSD6eYA3zr6aAK6GSbl8BCh/m5SAqUdAgTg==
   dependencies:
     call-bind "^1.0.2"
     define-properties "^1.1.3"
-    es-abstract "^1.18.0-next.1"
-    get-intrinsic "^1.0.2"
-    which-builtin-type "^1.0.1"
+    es-abstract "^1.18.6"
+    get-intrinsic "^1.1.1"
+    which-builtin-type "^1.1.1"
 
-regenerate-unicode-properties@^8.2.0:
-  version "8.2.0"
-  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
-  integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
+regenerate-unicode-properties@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
+  integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==
   dependencies:
-    regenerate "^1.4.0"
+    regenerate "^1.4.2"
 
-regenerate@^1.4.0:
+regenerate@^1.4.2:
   version "1.4.2"
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
   integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
@@ -7033,26 +7053,26 @@ regexpp@^3.1.0:
   integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
 
 regexpu-core@^4.7.1:
-  version "4.7.1"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
-  integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.8.0.tgz#e5605ba361b67b1718478501327502f4479a98f0"
+  integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==
   dependencies:
-    regenerate "^1.4.0"
-    regenerate-unicode-properties "^8.2.0"
-    regjsgen "^0.5.1"
-    regjsparser "^0.6.4"
-    unicode-match-property-ecmascript "^1.0.4"
-    unicode-match-property-value-ecmascript "^1.2.0"
+    regenerate "^1.4.2"
+    regenerate-unicode-properties "^9.0.0"
+    regjsgen "^0.5.2"
+    regjsparser "^0.7.0"
+    unicode-match-property-ecmascript "^2.0.0"
+    unicode-match-property-value-ecmascript "^2.0.0"
 
-regjsgen@^0.5.1:
+regjsgen@^0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
   integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
 
-regjsparser@^0.6.4:
-  version "0.6.9"
-  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.9.tgz#b489eef7c9a2ce43727627011429cf833a7183e6"
-  integrity sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==
+regjsparser@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.7.0.tgz#a6b667b54c885e18b52554cb4960ef71187e9968"
+  integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==
   dependencies:
     jsesc "~0.5.0"
 
@@ -7275,9 +7295,9 @@ sane@^4.0.3:
     walker "~1.0.5"
 
 sanitize-html@^2.3.2:
-  version "2.4.0"
-  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.4.0.tgz#8da7524332eb210d968971621b068b53f17ab5a3"
-  integrity sha512-Y1OgkUiTPMqwZNRLPERSEi39iOebn2XJLbeiGOBhaJD/yLqtLGu6GE5w7evx177LeGgSE+4p4e107LMiydOf6A==
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.5.1.tgz#f49998dc54c8180153940440d3a7294b09e4258a"
+  integrity sha512-hUITPitQk+eFNLtr4dEkaaiAJndG2YE87IOpcfBSL1XdklWgwcNDJdr9Ppe8QKL/C3jFt1xH/Mbj20e0GZQOfg==
   dependencies:
     deepmerge "^4.2.2"
     escape-string-regexp "^4.0.0"
@@ -7464,10 +7484,10 @@ source-map-resolve@^0.5.0:
     source-map-url "^0.4.0"
     urix "^0.1.0"
 
-source-map-support@^0.5.16, source-map-support@^0.5.6:
-  version "0.5.19"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
-  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+source-map-support@^0.5.16, source-map-support@^0.5.20, source-map-support@^0.5.6:
+  version "0.5.20"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
+  integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==
   dependencies:
     buffer-from "^1.0.0"
     source-map "^0.6.0"
@@ -7568,11 +7588,12 @@ stack-utils@^1.0.1:
     escape-string-regexp "^2.0.0"
 
 stack-utils@^2.0.2:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277"
-  integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.4.tgz#bf967ae2813d3d2d1e1f59a4408676495c8112ab"
+  integrity sha512-ERg+H//lSSYlZhBIUu+wJnqg30AbyBbpZlIhcshpn7BNzpoRODZgfyr9J+8ERf3ooC6af3u7Lcl01nleau7MrA==
   dependencies:
     escape-string-regexp "^2.0.0"
+    source-map-support "^0.5.20"
 
 static-extend@^0.1.1:
   version "0.1.2"
@@ -7725,9 +7746,9 @@ stylelint-config-standard@^20.0.0:
     stylelint-config-recommended "^3.0.0"
 
 stylelint-scss@^3.18.0:
-  version "3.20.1"
-  resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.20.1.tgz#88f175d9cfe1c81a72858bd0d3550cf61530e212"
-  integrity sha512-OTd55O1TTAC5nGKkVmUDLpz53LlK39R3MImv1CfuvsK7/qugktqiZAeQLuuC4UBhzxCnsc7fp9u/gfRZwFAIkA==
+  version "3.21.0"
+  resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-3.21.0.tgz#9f50898691b16b1c1ca3945837381d98c5b22331"
+  integrity sha512-CMI2wSHL+XVlNExpauy/+DbUcB/oUZLARDtMIXkpV/5yd8nthzylYd1cdHeDMJVBXeYHldsnebUX6MoV5zPW4A==
   dependencies:
     lodash "^4.17.15"
     postcss-media-query-parser "^0.2.3"
@@ -7898,9 +7919,9 @@ tmatch@^2.0.1:
   integrity sha1-DFYkbzPzDaG409colauvFmYPOM8=
 
 tmpl@1.0.x:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
-  integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
+  integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
 
 to-fast-properties@^2.0.0:
   version "2.0.0"
@@ -7983,7 +8004,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
-tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0:
+tslib@^2.0.0, tslib@^2.0.3, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
   integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
@@ -8068,7 +8089,7 @@ typedarray-to-buffer@^3.1.5:
   dependencies:
     is-typedarray "^1.0.0"
 
-typescript@^4.1.3:
+typescript@4.3.5:
   version "4.3.5"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
   integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
@@ -8093,28 +8114,28 @@ unhomoglyph@^1.0.6:
   resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3"
   integrity sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==
 
-unicode-canonical-property-names-ecmascript@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
-  integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
+unicode-canonical-property-names-ecmascript@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
+  integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==
 
-unicode-match-property-ecmascript@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
-  integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
+unicode-match-property-ecmascript@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3"
+  integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==
   dependencies:
-    unicode-canonical-property-names-ecmascript "^1.0.4"
-    unicode-property-aliases-ecmascript "^1.0.4"
+    unicode-canonical-property-names-ecmascript "^2.0.0"
+    unicode-property-aliases-ecmascript "^2.0.0"
 
-unicode-match-property-value-ecmascript@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
-  integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
+unicode-match-property-value-ecmascript@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714"
+  integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==
 
-unicode-property-aliases-ecmascript@^1.0.4:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
-  integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
+unicode-property-aliases-ecmascript@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8"
+  integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==
 
 unified@^9.1.0:
   version "9.2.2"
@@ -8316,15 +8337,15 @@ walker@^1.0.7, walker@~1.0.5:
     makeerror "1.0.x"
 
 webcrypto-core@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.2.0.tgz#44fda3f9315ed6effe9a1e47466e0935327733b5"
-  integrity sha512-p76Z/YLuE4CHCRdc49FB/ETaM4bzM3roqWNJeGs+QNY1fOTzKTOVnhmudW1fuO+5EZg6/4LG9NJ6gaAyxTk9XQ==
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.2.1.tgz#33f451a4c4faf159e74589436c80ca33998abad6"
+  integrity sha512-5+h1/e/A4eegCRTg+oQ9ehTJRTMwFhZazJ2RH1FP0VC3q1/0xl7x6SzzTwPxd/VTGc7kjuSEJGnfNgoLe5jNRQ==
   dependencies:
-    "@peculiar/asn1-schema" "^2.0.27"
+    "@peculiar/asn1-schema" "^2.0.38"
     "@peculiar/json-schema" "^1.1.12"
-    asn1js "^2.0.26"
-    pvtsutils "^1.1.2"
-    tslib "^2.1.0"
+    asn1js "^2.1.1"
+    pvtsutils "^1.2.0"
+    tslib "^2.3.1"
 
 webidl-conversions@^5.0.0:
   version "5.0.0"
@@ -8383,7 +8404,7 @@ which-boxed-primitive@^1.0.2:
     is-string "^1.0.5"
     is-symbol "^1.0.3"
 
-which-builtin-type@^1.0.1:
+which-builtin-type@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.1.tgz#1d14bb1b69b5680ebdddd7244689574678a1d83c"
   integrity sha512-zY3bUNzl/unBfSDS6ePT+/dwu6hZ7RMVMqHFvYxZEhisGEwCV/pYnXQ70nd3Hn2X6l8BNOWge5sHk3wAR3L42w==
@@ -8417,16 +8438,16 @@ which-module@^2.0.0:
   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
 
 which-typed-array@^1.1.5:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.6.tgz#f3713d801da0720a7f26f50c596980a9f5c8b383"
-  integrity sha512-DdY984dGD5sQ7Tf+x1CkXzdg85b9uEel6nr4UkFg1LoE9OXv3uRuZhe5CoWdawhGACeFpEZXH8fFLQnDhbpm/Q==
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.7.tgz#2761799b9a22d4b8660b3c1b40abaa7739691793"
+  integrity sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==
   dependencies:
-    available-typed-arrays "^1.0.4"
+    available-typed-arrays "^1.0.5"
     call-bind "^1.0.2"
     es-abstract "^1.18.5"
     foreach "^2.0.5"
     has-tostringtag "^1.0.0"
-    is-typed-array "^1.1.6"
+    is-typed-array "^1.1.7"
 
 which@^1.2.9, which@^1.3.1:
   version "1.3.1"
@@ -8490,9 +8511,9 @@ write-file-atomic@^3.0.0, write-file-atomic@^3.0.3:
     typedarray-to-buffer "^3.1.5"
 
 ws@^7.4.6:
-  version "7.5.3"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
-  integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
+  version "7.5.5"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
+  integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
 
 xml-name-validator@^3.0.0:
   version "3.0.0"