diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94909f9df4..07e478fa02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,19 @@
-Changes in [2.2.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.2) (2020-03-11)
+Changes in [2.2.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3) (2020-03-17)
 ===================================================================================================
-[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.1...v2.2.2)
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3-rc.1...v2.2.3)
+
+ * Upgrade JS SDK to 5.1.1
+ * Add default on config setting to control call button in composer
+   [\#4228](https://github.com/matrix-org/matrix-react-sdk/pull/4228)
+ * Fix: make alternative addresses UX less confusing
+   [\#4226](https://github.com/matrix-org/matrix-react-sdk/pull/4226)
+ * Fix: best-effort to join room without canonical alias over federation from
+   room directory
+   [\#4211](https://github.com/matrix-org/matrix-react-sdk/pull/4211)
+
+Changes in [2.2.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3-rc.1) (2020-03-11)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.1...v2.2.3-rc.1)
 
  * Update from Weblate
    [\#4200](https://github.com/matrix-org/matrix-react-sdk/pull/4200)
diff --git a/package.json b/package.json
index eeca614aa2..5999935517 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "2.2.2",
+  "version": "2.2.3",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {
@@ -72,7 +72,6 @@
     "flux": "2.1.1",
     "focus-visible": "^5.0.2",
     "fuse.js": "^2.2.0",
-    "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566",
     "gfm.css": "^1.1.1",
     "glob-to-regexp": "^0.4.1",
     "highlight.js": "^9.15.8",
@@ -93,7 +92,6 @@
     "react-beautiful-dnd": "^4.0.1",
     "react-dom": "^16.9.0",
     "react-focus-lock": "^2.2.1",
-    "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594",
     "resize-observer-polyfill": "^1.5.0",
     "sanitize-html": "^1.18.4",
     "text-encoding-utf-8": "^1.0.1",
diff --git a/res/css/_common.scss b/res/css/_common.scss
index a4ef603242..ad64aced50 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -207,37 +207,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
     transition: opacity 0.2s ease-in-out;
 }
 
-/* XXX: critical hack to GeminiScrollbar to allow them to work in FF 42 and Chrome 48.
-   Stop the scrollbar view from pushing out the container's overall sizing, which causes
-   flexbox to adapt to the new size and cause the view to keep growing.
- */
-.gm-scrollbar-container .gm-scroll-view {
-    position: absolute;
-}
-
-/* Expand thumbs on hoverover */
-.gm-scrollbar {
-    border-radius: 5px !important;
-}
-.gm-scrollbar.-vertical {
-    width: 6px;
-    transition: width 120ms ease-out !important;
-}
-.gm-scrollbar.-vertical:hover,
-.gm-scrollbar.-vertical:active {
-    width: 8px;
-    transition: width 120ms ease-out !important;
-}
-.gm-scrollbar.-horizontal {
-    height: 6px;
-    transition: height 120ms ease-out !important;
-}
-.gm-scrollbar.-horizontal:hover,
-.gm-scrollbar.-horizontal:active {
-    height: 8px;
-    transition: height 120ms ease-out !important;
-}
-
 // These are magic constants which are excluded from tinting, to let themes
 // (which only have CSS, unlike skins) tell the app what their non-tinted
 // colourscheme is by inspecting the stylesheet DOM.
diff --git a/res/css/_components.scss b/res/css/_components.scss
index bc636eb3c6..68322b9660 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -177,7 +177,6 @@
 @import "./views/rooms/_RoomTile.scss";
 @import "./views/rooms/_RoomUpgradeWarningBar.scss";
 @import "./views/rooms/_SearchBar.scss";
-@import "./views/rooms/_SearchableEntityList.scss";
 @import "./views/rooms/_SendMessageComposer.scss";
 @import "./views/rooms/_Stickers.scss";
 @import "./views/rooms/_TopUnreadMessagesBar.scss";
diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss
index 517b8b1922..2575169664 100644
--- a/res/css/structures/_GroupView.scss
+++ b/res/css/structures/_GroupView.scss
@@ -180,10 +180,6 @@ limitations under the License.
     line-height: 2em;
 }
 
-.mx_GroupView > .mx_MainSplit {
-    flex: 1;
-}
-
 .mx_GroupView_body {
     flex-grow: 1;
 }
@@ -341,8 +337,8 @@ limitations under the License.
     display: none;
 }
 
-.mx_GroupView_body .gm-scroll-view > * {
-    margin: 11px 50px 0px 68px;
+.mx_GroupView_body .mx_AutoHideScrollbar_offset > * {
+    margin: 11px 50px 50px 68px;
 }
 
 .mx_GroupView_groupDesc textarea {
@@ -370,7 +366,7 @@ limitations under the License.
     padding: 40px 20px;
 }
 
-.mx_GroupView .mx_MemberInfo .gm-scroll-view > :not(.mx_MemberInfo_avatar) {
+.mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar_offset > :not(.mx_MemberInfo_avatar) {
     padding-left: 16px;
     padding-right: 16px;
 }
diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss
index 4d73953cd7..25e1153fce 100644
--- a/res/css/structures/_MainSplit.scss
+++ b/res/css/structures/_MainSplit.scss
@@ -18,6 +18,7 @@ limitations under the License.
     display: flex;
     flex-direction: row;
     min-width: 0;
+    height: 100%;
 }
 
 // move hit area 5px to the right so it doesn't overlap with the timeline scrollbar
diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss
index f2ce7e1d5c..c5a5d50068 100644
--- a/res/css/structures/_MatrixChat.scss
+++ b/res/css/structures/_MatrixChat.scss
@@ -76,13 +76,6 @@ limitations under the License.
     flex: 1 1 0;
     min-width: 0;
 
-    /* Experimental fix for https://github.com/vector-im/vector-web/issues/947
-       and https://github.com/vector-im/vector-web/issues/946.
-       Empirically this stops the MessagePanel's width exploding outwards when
-       gemini is in 'prevented' mode
-       */
-    overflow-x: auto;
-
     /* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari
        needed height 100% all the way down to the HomePage. Height does not
        have to be auto, empirically.
diff --git a/res/css/structures/_MyGroups.scss b/res/css/structures/_MyGroups.scss
index d25789ab94..36150c33a5 100644
--- a/res/css/structures/_MyGroups.scss
+++ b/res/css/structures/_MyGroups.scss
@@ -67,9 +67,6 @@ limitations under the License.
     }
 }
 
-
-
-
 .mx_MyGroups_headerCard_header {
     font-weight: bold;
     margin-bottom: 10px;
@@ -98,6 +95,11 @@ limitations under the License.
 
     display: flex;
     flex-direction: column;
+    overflow-y: auto;
+}
+
+.mx_MyGroups_scrollable {
+    overflow-y: inherit;
 }
 
 .mx_MyGroups_placeholder {
diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss
index 5ae8df7176..f3a7b0e243 100644
--- a/res/css/structures/_RoomDirectory.scss
+++ b/res/css/structures/_RoomDirectory.scss
@@ -1,5 +1,6 @@
 /*
 Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -45,9 +46,8 @@ limitations under the License.
 }
 
 .mx_RoomDirectory_listheader {
-    display: flex;
-    margin-top: 12px;
-    margin-bottom: 12px;
+    display: block;
+    margin-top: 13px;
 }
 
 .mx_RoomDirectory_searchbox {
@@ -64,7 +64,7 @@ limitations under the License.
 }
 
 .mx_RoomDirectory_table {
-    font-size: 14px;
+    font-size: 12px;
     color: $primary-fg-color;
     width: 100%;
     text-align: left;
@@ -112,6 +112,7 @@ limitations under the License.
 
 .mx_RoomDirectory_name {
     display: inline-block;
+    font-size: 18px;
     font-weight: 600;
 }
 
@@ -148,8 +149,8 @@ limitations under the License.
     padding: 0;
 }
 
-.mx_RoomDirectory p {
-    font-size: 14px;
+.mx_RoomDirectory > span {
+    font-size: 15px;
     margin-top: 0;
 
     .mx_AccessibleButton {
diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss
index dddd2e324c..03f5ce230c 100644
--- a/res/css/structures/_TagPanel.scss
+++ b/res/css/structures/_TagPanel.scss
@@ -23,6 +23,7 @@ limitations under the License.
     flex-direction: column;
     align-items: center;
     justify-content: space-between;
+    height: 100%;
 }
 
 .mx_TagPanel_items_selected {
@@ -57,6 +58,7 @@ limitations under the License.
 
 .mx_TagPanel .mx_TagPanel_scroller {
     flex-grow: 1;
+    width: 100%;
 }
 
 .mx_TagPanel .mx_TagPanel_tagTileContainer {
diff --git a/res/css/views/dialogs/_UnknownDeviceDialog.scss b/res/css/views/dialogs/_UnknownDeviceDialog.scss
index 02e0fb1fe5..2b0f8dceca 100644
--- a/res/css/views/dialogs/_UnknownDeviceDialog.scss
+++ b/res/css/views/dialogs/_UnknownDeviceDialog.scss
@@ -14,14 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-// CSS voodoo to support a gemini-scrollbar for the contents of the dialog
-.mx_Dialog_unknownDevice .mx_Dialog {
-    // ideally we'd shrink the height to fit when needed, but in practice this
-    // is a pain in the ass. plus might as well make the dialog big given how
-    // important it is.
-    height: 100%;
-}
-
 .mx_UnknownDeviceDialog {
     height: 100%;
     display: flex;
@@ -44,6 +36,7 @@ limitations under the License.
 
 .mx_UnknownDeviceDialog .mx_Dialog_content {
     margin-bottom: 24px;
+    overflow-y: scroll;
 }
 
 .mx_UnknownDeviceDialog_deviceList > li {
diff --git a/res/css/views/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss
index d402f6c48f..106392f880 100644
--- a/res/css/views/directory/_NetworkDropdown.scss
+++ b/res/css/views/directory/_NetworkDropdown.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,70 +15,149 @@ limitations under the License.
 */
 
 .mx_NetworkDropdown {
+    height: 32px;
     position: relative;
-}
+    width: max-content;
+    padding-right: 32px;
+    margin-left: auto;
+    margin-right: 9px;
+    margin-top: 12px;
 
-.mx_NetworkDropdown_input {
-    position: relative;
-    border-radius: 3px;
-    border: 1px solid $strong-input-border-color;
-    font-weight: 300;
-    font-size: 13px;
-    user-select: none;
-}
-
-.mx_NetworkDropdown_arrow {
-    border-color: $primary-fg-color transparent transparent;
-    border-style: solid;
-    border-width: 5px 5px 0;
-    display: block;
-    height: 0;
-    position: absolute;
-    right: 10px;
-    top: 16px;
-    width: 0;
-}
-
-.mx_NetworkDropdown_networkoption {
-    height: 37px;
-    line-height: 37px;
-    padding-left: 8px;
-    padding-right: 8px;
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-}
-
-.mx_NetworkDropdown_networkoption img {
-    margin: 5px;
-    width: 25px;
-    vertical-align: middle;
-}
-
-input.mx_NetworkDropdown_networkoption, input.mx_NetworkDropdown_networkoption:focus {
-    border: 0;
-    padding-top: 0;
-    padding-bottom: 0;
+    .mx_AccessibleButton {
+        width: max-content;
+    }
 }
 
 .mx_NetworkDropdown_menu {
-    position: absolute;
-    left: -1px;
-    right: -1px;
-    top: 100%;
-    z-index: 2;
+    min-width: 204px;
     margin: 0;
-    padding: 0px;
-    border-radius: 3px;
-    border: 1px solid $accent-color;
+    box-sizing: border-box;
+    border-radius: 4px;
+    border: 1px solid $dialog-close-fg-color;
     background-color: $primary-bg-color;
 }
 
-.mx_NetworkDropdown_menu .mx_NetworkDropdown_networkoption:hover {
-    background-color: $focus-bg-color;
-}
-
 .mx_NetworkDropdown_menu_network {
     font-weight: bold;
 }
 
+.mx_NetworkDropdown_server {
+    padding: 12px 0;
+    border-bottom: 1px solid $input-darker-fg-color;
+
+    .mx_NetworkDropdown_server_title {
+        padding: 0 10px;
+        font-size: 15px;
+        font-weight: 600;
+        line-height: 20px;
+        margin-bottom: 4px;
+
+        // remove server button
+        .mx_AccessibleButton {
+            position: absolute;
+            display: inline;
+            right: 12px;
+            height: 16px;
+            width: 16px;
+            margin-top: 4px;
+
+            &::after {
+                content: "";
+                position: absolute;
+                width: 16px;
+                height: 16px;
+                mask-repeat: no-repeat;
+                mask-position: center;
+                mask-size: contain;
+                mask-image: url('$(res)/img/feather-customised/x.svg');
+                background-color: $notice-primary-color;
+            }
+        }
+    }
+
+    .mx_NetworkDropdown_server_subtitle {
+        padding: 0 10px;
+        font-size: 10px;
+        line-height: 14px;
+        margin-top: -4px;
+        margin-bottom: 4px;
+        color: $muted-fg-color;
+    }
+
+    .mx_NetworkDropdown_server_network {
+        font-size: 12px;
+        line-height: 16px;
+        padding: 4px 10px;
+        cursor: pointer;
+        position: relative;
+
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+
+        &[aria-checked=true]::after {
+            content: "";
+            position: absolute;
+            width: 16px;
+            height: 16px;
+            right: 10px;
+            mask-repeat: no-repeat;
+            mask-position: center;
+            mask-size: contain;
+            mask-image: url('$(res)/img/feather-customised/check.svg');
+            background-color: $input-valid-border-color;
+        }
+    }
+}
+
+.mx_NetworkDropdown_server_add,
+.mx_NetworkDropdown_server_network {
+    &:hover {
+        background-color: $header-panel-bg-color;
+    }
+}
+
+.mx_NetworkDropdown_server_add {
+    padding: 16px 10px 16px 32px;
+    position: relative;
+    border-radius: 0 0 4px 4px;
+
+    &::before {
+        content: "";
+        position: absolute;
+        width: 16px;
+        height: 16px;
+        left: 7px;
+        mask-repeat: no-repeat;
+        mask-position: center;
+        mask-size: contain;
+        mask-image: url('$(res)/img/feather-customised/plus.svg');
+        background-color: $muted-fg-color;
+    }
+}
+
+.mx_NetworkDropdown_handle {
+    position: relative;
+
+    &::after {
+        content: "";
+        position: absolute;
+        width: 24px;
+        height: 24px;
+        right: -28px; // - (24 + 4)
+        mask-repeat: no-repeat;
+        mask-position: center;
+        mask-size: contain;
+        mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
+        background-color: $primary-fg-color;
+    }
+
+    .mx_NetworkDropdown_handle_server {
+        color: $muted-fg-color;
+        font-size: 12px;
+    }
+}
+
+.mx_NetworkDropdown_dialog .mx_Dialog {
+    width: 45vw;
+}
diff --git a/res/css/views/elements/_DirectorySearchBox.scss b/res/css/views/elements/_DirectorySearchBox.scss
index ef944f6fa0..75ef3fbabd 100644
--- a/res/css/views/elements/_DirectorySearchBox.scss
+++ b/res/css/views/elements/_DirectorySearchBox.scss
@@ -18,7 +18,6 @@ limitations under the License.
     display: flex;
     padding-left: 9px;
     padding-right: 9px;
-    margin: 0 5px 0 0 !important;
 }
 
 .mx_DirectorySearchBox_joinButton {
diff --git a/res/css/views/rooms/_SearchableEntityList.scss b/res/css/views/rooms/_SearchableEntityList.scss
deleted file mode 100644
index 37a663123d..0000000000
--- a/res/css/views/rooms/_SearchableEntityList.scss
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-.mx_SearchableEntityList {
-    display: flex;
-
-    flex-direction: column;
-}
-
-.mx_SearchableEntityList_query {
-    font-family: $font-family;
-    border-radius: 3px;
-    border: 1px solid $input-border-color;
-    padding: 9px;
-    color: $primary-fg-color;
-    background-color: $primary-bg-color;
-    margin-left: 3px;
-    font-size: 15px;
-    margin-bottom: 8px;
-    width: 189px;
-}
-
-.mx_SearchableEntityList_query::-moz-placeholder {
-    color: $primary-fg-color;
-    opacity: 0.5;
-    font-size: 12px;
-}
-
-.mx_SearchableEntityList_query::-webkit-input-placeholder {
-    color: $primary-fg-color;
-    opacity: 0.5;
-    font-size: 12px;
-}
-
-.mx_SearchableEntityList_listWrapper {
-    flex: 1;
-
-    overflow-y: auto;
-}
-
-.mx_SearchableEntityList_list {
-    display: table;
-    table-layout: fixed;
-    width: 100%;
-}
-
-.mx_SearchableEntityList_list .mx_EntityTile_chevron {
-    display: none;
-}
-
-.mx_SearchableEntityList_hrWrapper {
-    width: 100%;
-    flex: 0 0 auto;
-}
-
-.mx_SearchableEntityList hr {
-    height: 1px;
-    border: 0px;
-    color: $primary-fg-color;
-    background-color: $primary-fg-color;
-    margin-right: 15px;
-    margin-top: 11px;
-    margin-bottom: 11px;
-}
diff --git a/res/img/feather-customised/chevron-down.svg b/res/img/feather-customised/chevron-down.svg
new file mode 100644
index 0000000000..bcb185ede7
--- /dev/null
+++ b/res/img/feather-customised/chevron-down.svg
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 9L12 15L18 9" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index a3515a9d99..33670c39bf 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -219,10 +219,6 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
     filter: invert(1);
 }
 
-.gm-scrollbar .thumb {
-    filter: invert(1);
-}
-
 // markdown overrides:
 .mx_EventTile_content .markdown-body pre:hover {
     border-color: #808080 !important; // inverted due to rules below
diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js
index 898991f4f2..b4647a6c30 100644
--- a/src/components/structures/ContextMenu.js
+++ b/src/components/structures/ContextMenu.js
@@ -350,7 +350,7 @@ export const ContextMenuButton = ({ label, isExpanded, children, ...props }) =>
 };
 ContextMenuButton.propTypes = {
     ...AccessibleButton.propTypes,
-    label: PropTypes.string.isRequired,
+    label: PropTypes.string,
     isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open
 };
 
@@ -377,7 +377,6 @@ export const MenuGroup = ({children, label, ...props}) => {
     </div>;
 };
 MenuGroup.propTypes = {
-    ...AccessibleButton.propTypes,
     label: PropTypes.string.isRequired,
     className: PropTypes.string, // optional
 };
diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js
index 6d734c3838..f854dc955f 100644
--- a/src/components/structures/EmbeddedPage.js
+++ b/src/components/structures/EmbeddedPage.js
@@ -23,11 +23,11 @@ import PropTypes from 'prop-types';
 import request from 'browser-request';
 import { _t } from '../../languageHandler';
 import sanitizeHtml from 'sanitize-html';
-import * as sdk from '../../index';
 import dis from '../../dispatcher';
 import {MatrixClientPeg} from '../../MatrixClientPeg';
 import classnames from 'classnames';
 import MatrixClientContext from "../../contexts/MatrixClientContext";
+import AutoHideScrollbar from "./AutoHideScrollbar";
 
 export default class EmbeddedPage extends React.PureComponent {
     static propTypes = {
@@ -117,10 +117,9 @@ export default class EmbeddedPage extends React.PureComponent {
         </div>;
 
         if (this.props.scrollbar) {
-            const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
-            return <GeminiScrollbarWrapper autoshow={true} className={classes}>
+            return <AutoHideScrollbar className={classes}>
                 {content}
-            </GeminiScrollbarWrapper>;
+            </AutoHideScrollbar>;
         } else {
             return <div className={classes}>
                 {content}
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index af90fbbe83..cadc511fc3 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -39,6 +39,7 @@ import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Perm
 import {Group} from "matrix-js-sdk";
 import {allSettled, sleep} from "../../utils/promise";
 import RightPanelStore from "../../stores/RightPanelStore";
+import AutoHideScrollbar from "./AutoHideScrollbar";
 
 const LONG_DESC_PLACEHOLDER = _td(
 `<h1>HTML for your community's page</h1>
@@ -1173,7 +1174,6 @@ export default createReactClass({
     render: function() {
         const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
         const Spinner = sdk.getComponent("elements.Spinner");
-        const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
 
         if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
             return <Spinner />;
@@ -1332,10 +1332,10 @@ export default createReactClass({
                         <GroupHeaderButtons />
                     </div>
                     <MainSplit panel={rightPanel}>
-                        <GeminiScrollbarWrapper className="mx_GroupView_body">
+                        <AutoHideScrollbar className="mx_GroupView_body">
                             { this._getMembershipSection() }
                             { this._getGroupSection() }
-                        </GeminiScrollbarWrapper>
+                        </AutoHideScrollbar>
                     </MainSplit>
                 </main>
             );
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index f1a5a372be..3e59112a63 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -600,9 +600,8 @@ export default createReactClass({
             break;
             case 'view_room_directory': {
                 const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
-                Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
-                    config: this.props.config,
-                }, 'mx_RoomDirectory_dialogWrapper');
+                Modal.createTrackedDialog('Room directory', '', RoomDirectory, {},
+                    'mx_RoomDirectory_dialogWrapper', false, true);
 
                 // View the welcome or home page if we need something to look at
                 this._viewSomethingBehindModal();
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index b26ab5ff70..f1209b7b9e 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -22,6 +22,7 @@ import { _t } from '../../languageHandler';
 import dis from '../../dispatcher';
 import AccessibleButton from '../views/elements/AccessibleButton';
 import MatrixClientContext from "../../contexts/MatrixClientContext";
+import AutoHideScrollbar from "./AutoHideScrollbar";
 
 export default createReactClass({
     displayName: 'MyGroups',
@@ -62,8 +63,6 @@ export default createReactClass({
         const Loader = sdk.getComponent("elements.Spinner");
         const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
         const GroupTile = sdk.getComponent("groups.GroupTile");
-        const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
-
 
         let content;
         let contentHeader;
@@ -74,7 +73,7 @@ export default createReactClass({
             });
             contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
             content = groupNodes.length > 0 ?
-                <GeminiScrollbarWrapper>
+                <AutoHideScrollbar className="mx_MyGroups_scrollable">
                     <div className="mx_MyGroups_microcopy">
                         <p>
                             { _t(
@@ -93,7 +92,7 @@ export default createReactClass({
                     <div className="mx_MyGroups_joinedGroups">
                         { groupNodes }
                     </div>
-                </GeminiScrollbarWrapper> :
+                </AutoHideScrollbar> :
                 <div className="mx_MyGroups_placeholder">
                     { _t(
                         "You're not currently a member of any communities.",
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index 6310efffb9..664aaaf21f 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -28,6 +28,7 @@ import { _t } from '../../languageHandler';
 import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
 import Analytics from '../../Analytics';
 import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
+import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
 
 const MAX_NAME_LENGTH = 80;
 const MAX_TOPIC_LENGTH = 160;
@@ -40,25 +41,17 @@ export default createReactClass({
     displayName: 'RoomDirectory',
 
     propTypes: {
-        config: PropTypes.object,
         onFinished: PropTypes.func.isRequired,
     },
 
-    getDefaultProps: function() {
-        return {
-            config: {},
-        };
-    },
-
     getInitialState: function() {
         return {
             publicRooms: [],
             loading: true,
             protocolsLoading: true,
             error: null,
-            instanceId: null,
-            includeAll: false,
-            roomServer: null,
+            instanceId: undefined,
+            roomServer: MatrixClientPeg.getHomeserverName(),
             filterString: null,
         };
     },
@@ -98,6 +91,10 @@ export default createReactClass({
         });
     },
 
+    componentDidMount: function() {
+        this.refreshRoomList();
+    },
+
     componentWillUnmount: function() {
         if (this.filterTimeout) {
             clearTimeout(this.filterTimeout);
@@ -130,10 +127,10 @@ export default createReactClass({
         if (my_server != MatrixClientPeg.getHomeserverName()) {
             opts.server = my_server;
         }
-        if (this.state.instanceId) {
-            opts.third_party_instance_id = this.state.instanceId;
-        } else if (this.state.includeAll) {
+        if (this.state.instanceId === ALL_ROOMS) {
             opts.include_all_networks = true;
+        } else if (this.state.instanceId) {
+            opts.third_party_instance_id = this.state.instanceId;
         }
         if (this.nextBatch) opts.since = this.nextBatch;
         if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
@@ -247,7 +244,7 @@ export default createReactClass({
         }
     },
 
-    onOptionChange: function(server, instanceId, includeAll) {
+    onOptionChange: function(server, instanceId) {
         // clear next batch so we don't try to load more rooms
         this.nextBatch = null;
         this.setState({
@@ -257,7 +254,6 @@ export default createReactClass({
             publicRooms: [],
             roomServer: server,
             instanceId: instanceId,
-            includeAll: includeAll,
             error: null,
         }, this.refreshRoomList);
         // We also refresh the room list each time even though this
@@ -305,7 +301,7 @@ export default createReactClass({
 
     onJoinFromSearchClick: function(alias) {
         // If we don't have a particular instance id selected, just show that rooms alias
-        if (!this.state.instanceId) {
+        if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
             // If the user specified an alias without a domain, add on whichever server is selected
             // in the dropdown
             if (alias.indexOf(':') == -1) {
@@ -593,7 +589,7 @@ export default createReactClass({
             }
 
             let placeholder = _t('Find a room…');
-            if (!this.state.instanceId) {
+            if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
                 placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
             } else if (instance_expected_field_type) {
                 placeholder = instance_expected_field_type.placeholder;
@@ -610,10 +606,18 @@ export default createReactClass({
             listHeader = <div className="mx_RoomDirectory_listheader">
                 <DirectorySearchBox
                     className="mx_RoomDirectory_searchbox"
-                    onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinFromSearchClick}
-                    placeholder={placeholder} showJoinButton={showJoinButton}
+                    onChange={this.onFilterChange}
+                    onClear={this.onFilterClear}
+                    onJoinClick={this.onJoinFromSearchClick}
+                    placeholder={placeholder}
+                    showJoinButton={showJoinButton}
+                />
+                <NetworkDropdown
+                    protocols={this.protocols}
+                    onOptionChange={this.onOptionChange}
+                    selectedServerName={this.state.roomServer}
+                    selectedInstanceId={this.state.instanceId}
                 />
-                <NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
             </div>;
         }
         const explanation =
@@ -634,7 +638,7 @@ export default createReactClass({
                 title={_t("Explore rooms")}
             >
                 <div className="mx_RoomDirectory">
-                    <p>{explanation}</p>
+                    {explanation}
                     <div className="mx_RoomDirectory_list">
                         {listHeader}
                         {content}
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 36e30343e4..17a496b037 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -405,21 +405,6 @@ export default createReactClass({
         this.onResize();
 
         document.addEventListener("keydown", this.onKeyDown);
-
-        // XXX: EVIL HACK to autofocus inviting on empty rooms.
-        // We use the setTimeout to avoid racing with focus_composer.
-        if (this.state.room &&
-            this.state.room.getJoinedMemberCount() == 1 &&
-            this.state.room.getLiveTimeline() &&
-            this.state.room.getLiveTimeline().getEvents() &&
-            this.state.room.getLiveTimeline().getEvents().length <= 6) {
-            const inviteBox = document.getElementById("mx_SearchableEntityList_query");
-            setTimeout(function() {
-                if (inviteBox) {
-                    inviteBox.focus();
-                }
-            }, 50);
-        }
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js
index b81b3ebede..c218fee5d6 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.js
@@ -782,7 +782,7 @@ export default createReactClass({
         if (!this._divScroll) {
             // Likewise, we should have the ref by this point, but if not
             // turn the NPE into something meaningful.
-            throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
+            throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
         }
 
         return this._divScroll;
diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js
index 622e63d8ce..f1a39d6fcf 100644
--- a/src/components/structures/TagPanel.js
+++ b/src/components/structures/TagPanel.js
@@ -28,6 +28,7 @@ import { _t } from '../../languageHandler';
 import { Droppable } from 'react-beautiful-dnd';
 import classNames from 'classnames';
 import MatrixClientContext from "../../contexts/MatrixClientContext";
+import AutoHideScrollbar from "./AutoHideScrollbar";
 
 const TagPanel = createReactClass({
     displayName: 'TagPanel',
@@ -106,7 +107,6 @@ const TagPanel = createReactClass({
         const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
         const ActionButton = sdk.getComponent('elements.ActionButton');
         const TintableSvg = sdk.getComponent('elements.TintableSvg');
-        const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
 
         const tags = this.state.orderedTags.map((tag, index) => {
             return <DNDTagTile
@@ -138,9 +138,8 @@ const TagPanel = createReactClass({
                 { clearButton }
             </div>
             <div className="mx_TagPanel_divider" />
-            <GeminiScrollbarWrapper
+            <AutoHideScrollbar
                 className="mx_TagPanel_scroller"
-                autoshow={true}
                 // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
                 // instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6253
                 onMouseDown={this.onMouseDown}
@@ -166,7 +165,7 @@ const TagPanel = createReactClass({
                             </div>
                     ) }
                 </Droppable>
-            </GeminiScrollbarWrapper>
+            </AutoHideScrollbar>
         </div>;
     },
 });
diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js
index e8a5e3db49..3154564cd3 100644
--- a/src/components/structures/auth/CompleteSecurity.js
+++ b/src/components/structures/auth/CompleteSecurity.js
@@ -63,9 +63,30 @@ export default class CompleteSecurity extends React.Component {
             const backupInfo = await cli.getKeyBackupVersion();
             this.setState({backupInfo});
 
-            await accessSecretStorage(async () => {
-                await cli.checkOwnCrossSigningTrust();
-                if (backupInfo) await cli.restoreKeyBackupWithSecretStorage(backupInfo);
+            // The control flow is fairly twisted here...
+            // For the purposes of completing security, we only wait on getting
+            // as far as the trust check and then show a green shield.
+            // We also begin the key backup restore as well, which we're
+            // awaiting inside `accessSecretStorage` only so that it keeps your
+            // passphase cached for that work. This dialog itself will only wait
+            // on the first trust check, and the key backup restore will happen
+            // in the background.
+            await new Promise((resolve, reject) => {
+                try {
+                    accessSecretStorage(async () => {
+                        await cli.checkOwnCrossSigningTrust();
+                        resolve();
+                        if (backupInfo) {
+                            // A complete restore can take many minutes for large
+                            // accounts / slow servers, so we allow the dialog
+                            // to advance before this.
+                            await cli.restoreKeyBackupWithSecretStorage(backupInfo);
+                        }
+                    });
+                } catch (e) {
+                    console.error(e);
+                    reject(e);
+                }
             });
 
             if (cli.getCrossSigningId()) {
diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js
index 8cb16dd88f..07a1eae5d5 100644
--- a/src/components/views/dialogs/QuestionDialog.js
+++ b/src/components/views/dialogs/QuestionDialog.js
@@ -33,6 +33,7 @@ export default createReactClass({
         onFinished: PropTypes.func.isRequired,
         headerImage: PropTypes.string,
         quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
+        fixedWidth: PropTypes.bool,
     },
 
     getDefaultProps: function() {
@@ -63,11 +64,14 @@ export default createReactClass({
             primaryButtonClass = "danger";
         }
         return (
-            <BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
+            <BaseDialog
+                className="mx_QuestionDialog"
+                onFinished={this.props.onFinished}
                 title={this.props.title}
                 contentId='mx_Dialog_content'
                 headerImage={this.props.headerImage}
                 hasCancel={this.props.hasCancelButton}
+                fixedWidth={this.props.fixedWidth}
             >
                 <div className="mx_Dialog_content" id='mx_Dialog_content'>
                     { this.props.description }
diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js
index 0ffc072cc0..b9f6f6ebce 100644
--- a/src/components/views/dialogs/TextInputDialog.js
+++ b/src/components/views/dialogs/TextInputDialog.js
@@ -18,6 +18,7 @@ import React, {createRef} from 'react';
 import createReactClass from 'create-react-class';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
+import Field from "../elements/Field";
 
 export default createReactClass({
     displayName: 'TextInputDialog',
@@ -28,9 +29,13 @@ export default createReactClass({
             PropTypes.string,
         ]),
         value: PropTypes.string,
+        placeholder: PropTypes.string,
         button: PropTypes.string,
         focus: PropTypes.bool,
         onFinished: PropTypes.func.isRequired,
+        hasCancel: PropTypes.bool,
+        validator: PropTypes.func, // result of withValidation
+        fixedWidth: PropTypes.bool,
     },
 
     getDefaultProps: function() {
@@ -39,34 +44,70 @@ export default createReactClass({
             value: "",
             description: "",
             focus: true,
+            hasCancel: true,
+        };
+    },
+
+    getInitialState: function() {
+        return {
+            value: this.props.value,
+            valid: false,
         };
     },
 
     UNSAFE_componentWillMount: function() {
-        this._textinput = createRef();
+        this._field = createRef();
     },
 
     componentDidMount: function() {
         if (this.props.focus) {
             // Set the cursor at the end of the text input
-            this._textinput.current.value = this.props.value;
+            // this._field.current.value = this.props.value;
+            this._field.current.focus();
         }
     },
 
-    onOk: function() {
-        this.props.onFinished(true, this._textinput.current.value);
+    onOk: async function(ev) {
+        ev.preventDefault();
+        if (this.props.validator) {
+            await this._field.current.validate({ allowEmpty: false });
+
+            if (!this._field.current.state.valid) {
+                this._field.current.focus();
+                this._field.current.validate({ allowEmpty: false, focused: true });
+                return;
+            }
+        }
+        this.props.onFinished(true, this.state.value);
     },
 
     onCancel: function() {
         this.props.onFinished(false);
     },
 
+    onChange: function(ev) {
+        this.setState({
+            value: ev.target.value,
+        });
+    },
+
+    onValidate: async function(fieldState) {
+        const result = await this.props.validator(fieldState);
+        this.setState({
+            valid: result.valid,
+        });
+        return result;
+    },
+
     render: function() {
         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
         const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
         return (
-            <BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
+            <BaseDialog
+                className="mx_TextInputDialog"
+                onFinished={this.props.onFinished}
                 title={this.props.title}
+                fixedWidth={this.props.fixedWidth}
             >
                 <form onSubmit={this.onOk}>
                     <div className="mx_Dialog_content">
@@ -74,19 +115,26 @@ export default createReactClass({
                             <label htmlFor="textinput"> { this.props.description } </label>
                         </div>
                         <div>
-                            <input
-                                id="textinput"
-                                ref={this._textinput}
+                            <Field
+                                id="mx_TextInputDialog_field"
                                 className="mx_TextInputDialog_input"
-                                defaultValue={this.props.value}
-                                autoFocus={this.props.focus}
-                                size="64" />
+                                ref={this._field}
+                                type="text"
+                                label={this.props.placeholder}
+                                value={this.state.value}
+                                onChange={this.onChange}
+                                onValidate={this.props.validator ? this.onValidate : undefined}
+                                size="64"
+                            />
                         </div>
                     </div>
                 </form>
-                <DialogButtons primaryButton={this.props.button}
+                <DialogButtons
+                    primaryButton={this.props.button}
                     onPrimaryButtonClick={this.onOk}
-                    onCancel={this.onCancel} />
+                    onCancel={this.onCancel}
+                    hasCancel={this.props.hasCancel}
+                />
             </BaseDialog>
         );
     },
diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js
index d58961f964..69ebb72a6f 100644
--- a/src/components/views/dialogs/UnknownDeviceDialog.js
+++ b/src/components/views/dialogs/UnknownDeviceDialog.js
@@ -122,7 +122,6 @@ export default createReactClass({
     },
 
     render: function() {
-        const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
         if (this.props.devices === null) {
             const Spinner = sdk.getComponent("elements.Spinner");
             return <Spinner />;
@@ -168,7 +167,7 @@ export default createReactClass({
                 title={_t('Room contains unknown sessions')}
                 contentId='mx_Dialog_content'
             >
-                <GeminiScrollbarWrapper autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
+                <div className="mx_Dialog_content" id='mx_Dialog_content'>
                     <h4>
                         { _t('"%(RoomName)s" contains sessions that you haven\'t seen before.', {RoomName: this.props.room.name}) }
                     </h4>
@@ -176,7 +175,7 @@ export default createReactClass({
                     { _t("Unknown sessions") }:
 
                     <UnknownDeviceList devices={this.props.devices} />
-                </GeminiScrollbarWrapper>
+                </div>
                 <DialogButtons primaryButton={sendButtonLabel}
                     onPrimaryButtonClick={sendButtonOnClick}
                     onCancel={this._onDismissClicked} />
diff --git a/src/components/views/directory/NetworkDropdown.js b/src/components/views/directory/NetworkDropdown.js
index cb6a015d86..2fabda1a74 100644
--- a/src/components/views/directory/NetworkDropdown.js
+++ b/src/components/views/directory/NetworkDropdown.js
@@ -1,6 +1,7 @@
 /*
 Copyright 2016 OpenMarket Ltd
 Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+Copyright 2020 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -15,241 +16,275 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
+import React, {useEffect, useState} from 'react';
 import PropTypes from 'prop-types';
+
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
+import {
+    ContextMenu,
+    useContextMenu,
+    ContextMenuButton,
+    MenuItemRadio,
+    MenuItem,
+    MenuGroup,
+} from "../../structures/ContextMenu";
+import {_t} from "../../../languageHandler";
+import SdkConfig from "../../../SdkConfig";
+import {useSettingValue} from "../../../hooks/useSettings";
+import * as sdk from "../../../index";
+import Modal from "../../../Modal";
+import SettingsStore from "../../../settings/SettingsStore";
+import withValidation from "../elements/Validation";
 
-const DEFAULT_ICON_URL = require("../../../../res/img/network-matrix.svg");
+export const ALL_ROOMS = Symbol("ALL_ROOMS");
 
-export default class NetworkDropdown extends React.Component {
-    constructor(props) {
-        super(props);
+const SETTING_NAME = "room_directory_servers";
 
-        this.dropdownRootElement = null;
-        this.ignoreEvent = null;
+const inPlaceOf = (elementRect) => ({
+    right: window.innerWidth - elementRect.right,
+    top: elementRect.top,
+    chevronOffset: 0,
+    chevronFace: "none",
+});
 
-        this.onInputClick = this.onInputClick.bind(this);
-        this.onRootClick = this.onRootClick.bind(this);
-        this.onDocumentClick = this.onDocumentClick.bind(this);
-        this.onMenuOptionClick = this.onMenuOptionClick.bind(this);
-        this.onInputKeyUp = this.onInputKeyUp.bind(this);
-        this.collectRoot = this.collectRoot.bind(this);
-        this.collectInputTextBox = this.collectInputTextBox.bind(this);
+const validServer = withValidation({
+    rules: [
+        {
+            key: "required",
+            test: async ({ value }) => !!value,
+            invalid: () => _t("Enter a server name"),
+        }, {
+            key: "available",
+            final: true,
+            test: async ({ value }) => {
+                try {
+                    const opts = {
+                        limit: 1,
+                        server: value,
+                    };
+                    // check if we can successfully load this server's room directory
+                    await MatrixClientPeg.get().publicRooms(opts);
+                    return true;
+                } catch (e) {
+                    return false;
+                }
+            },
+            valid: () => _t("Looks good"),
+            invalid: () => _t("Can't find this server or its room list"),
+        },
+    ],
+});
 
-        this.inputTextBox = null;
+// This dropdown sources homeservers from three places:
+// + your currently connected homeserver
+// + homeservers in config.json["roomDirectory"]
+// + homeservers in SettingsStore["room_directory_servers"]
+// if a server exists in multiple, only keep the top-most entry.
 
-        const server = MatrixClientPeg.getHomeserverName();
-        this.state = {
-            expanded: false,
-            selectedServer: server,
-            selectedInstanceId: null,
-            includeAllNetworks: false,
+const NetworkDropdown = ({onOptionChange, protocols = {}, selectedServerName, selectedInstanceId}) => {
+    const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
+    const _userDefinedServers = useSettingValue(SETTING_NAME);
+    const [userDefinedServers, _setUserDefinedServers] = useState(_userDefinedServers);
+
+    const handlerFactory = (server, instanceId) => {
+        return () => {
+            onOptionChange(server, instanceId);
+            closeMenu();
         };
-    }
+    };
 
-    componentWillMount() {
-        // Listen for all clicks on the document so we can close the
-        // menu when the user clicks somewhere else
-        document.addEventListener('click', this.onDocumentClick, false);
+    const setUserDefinedServers = servers => {
+        _setUserDefinedServers(servers);
+        SettingsStore.setValue(SETTING_NAME, null, "account", servers);
+    };
+    // keep local echo up to date with external changes
+    useEffect(() => {
+        _setUserDefinedServers(_userDefinedServers);
+    }, [_userDefinedServers]);
 
-        // fire this now so the defaults can be set up
-        const {selectedServer, selectedInstanceId, includeAllNetworks} = this.state;
-        this.props.onOptionChange(selectedServer, selectedInstanceId, includeAllNetworks);
-    }
+    // we either show the button or the dropdown in its place.
+    let content;
+    if (menuDisplayed) {
+        const config = SdkConfig.get();
+        const roomDirectory = config.roomDirectory || {};
 
-    componentWillUnmount() {
-        document.removeEventListener('click', this.onDocumentClick, false);
-    }
+        const hsName = MatrixClientPeg.getHomeserverName();
+        const configServers = new Set(roomDirectory.servers);
 
-    componentDidUpdate() {
-        if (this.state.expanded && this.inputTextBox) {
-            this.inputTextBox.focus();
-        }
-    }
-
-    onDocumentClick(ev) {
-        // Close the dropdown if the user clicks anywhere that isn't
-        // within our root element
-        if (ev !== this.ignoreEvent) {
-            this.setState({
-                expanded: false,
-            });
-        }
-    }
-
-    onRootClick(ev) {
-        // This captures any clicks that happen within our elements,
-        // such that we can then ignore them when they're seen by the
-        // click listener on the document handler, ie. not close the
-        // dropdown immediately after opening it.
-        // NB. We can't just stopPropagation() because then the event
-        // doesn't reach the React onClick().
-        this.ignoreEvent = ev;
-    }
-
-    onInputClick(ev) {
-        this.setState({
-            expanded: !this.state.expanded,
-        });
-        ev.preventDefault();
-    }
-
-    onMenuOptionClick(server, instance, includeAll) {
-        this.setState({
-            expanded: false,
-            selectedServer: server,
-            selectedInstanceId: instance ? instance.instance_id : null,
-            includeAllNetworks: includeAll,
-        });
-        this.props.onOptionChange(server, instance ? instance.instance_id : null, includeAll);
-    }
-
-    onInputKeyUp(e) {
-        if (e.key === 'Enter') {
-            this.setState({
-                expanded: false,
-                selectedServer: e.target.value,
-                selectedNetwork: null,
-                includeAllNetworks: false,
-            });
-            this.props.onOptionChange(e.target.value, null);
-        }
-    }
-
-    collectRoot(e) {
-        if (this.dropdownRootElement) {
-            this.dropdownRootElement.removeEventListener('click', this.onRootClick, false);
-        }
-        if (e) {
-            e.addEventListener('click', this.onRootClick, false);
-        }
-        this.dropdownRootElement = e;
-    }
-
-    collectInputTextBox(e) {
-        this.inputTextBox = e;
-    }
-
-    _getMenuOptions() {
-        const options = [];
-        const roomDirectory = this.props.config.roomDirectory || {};
-
-        let servers = [];
-        if (roomDirectory.servers) {
-            servers = servers.concat(roomDirectory.servers);
-        }
-
-        if (!servers.includes(MatrixClientPeg.getHomeserverName())) {
-            servers.unshift(MatrixClientPeg.getHomeserverName());
-        }
+        // configured servers take preference over user-defined ones, if one occurs in both ignore the latter one.
+        const removableServers = new Set(userDefinedServers.filter(s => !configServers.has(s) && s !== hsName));
+        const servers = [
+            // we always show our connected HS, this takes precedence over it being configured or user-defined
+            hsName,
+            ...Array.from(configServers).filter(s => s !== hsName).sort(),
+            ...Array.from(removableServers).sort(),
+        ];
 
         // For our own HS, we can use the instance_ids given in the third party protocols
         // response to get the server to filter the room list by network for us.
         // We can't get thirdparty protocols for remote server yet though, so for those
         // we can only show the default room list.
-        for (const server of servers) {
-            options.push(this._makeMenuOption(server, null, true));
-            if (server === MatrixClientPeg.getHomeserverName()) {
-                options.push(this._makeMenuOption(server, null, false));
-                if (this.props.protocols) {
-                    for (const proto of Object.keys(this.props.protocols)) {
-                        if (!this.props.protocols[proto].instances) continue;
+        const options = servers.map(server => {
+            const serverSelected = server === selectedServerName;
+            const entries = [];
 
-                        const sortedInstances = this.props.protocols[proto].instances;
-                        sortedInstances.sort(function(x, y) {
-                            const a = x.desc;
-                            const b = y.desc;
-                            if (a < b) {
-                                return -1;
-                            } else if (a > b) {
-                                return 1;
-                            } else {
-                                return 0;
-                            }
-                        });
-
-                        for (const instance of sortedInstances) {
-                            if (!instance.instance_id) continue;
-                            options.push(this._makeMenuOption(server, instance, false));
-                        }
-                    }
-                }
+            const protocolsList = server === hsName ? Object.values(protocols) : [];
+            if (protocolsList.length > 0) {
+                // add a fake protocol with the ALL_ROOMS symbol
+                protocolsList.push({
+                    instances: [{
+                        instance_id: ALL_ROOMS,
+                        desc: _t("All rooms"),
+                    }],
+                });
             }
-        }
 
-        return options;
-    }
+            protocolsList.forEach(({instances=[]}) => {
+                [...instances].sort((b, a) => {
+                    return a.desc.localeCompare(b.desc);
+                }).forEach(({desc, instance_id: instanceId}) => {
+                    entries.push(
+                        <MenuItemRadio
+                            key={String(instanceId)}
+                            active={serverSelected && instanceId === selectedInstanceId}
+                            onClick={handlerFactory(server, instanceId)}
+                            label={desc}
+                            className="mx_NetworkDropdown_server_network"
+                        >
+                            { desc }
+                        </MenuItemRadio>);
+                });
+            });
 
-    _makeMenuOption(server, instance, includeAll, handleClicks) {
-        if (handleClicks === undefined) handleClicks = true;
+            let subtitle;
+            if (server === hsName) {
+                subtitle = (
+                    <div className="mx_NetworkDropdown_server_subtitle">
+                        {_t("Your server")}
+                    </div>
+                );
+            }
 
-        let icon;
-        let name;
-        let key;
+            let removeButton;
+            if (removableServers.has(server)) {
+                const onClick = async () => {
+                    closeMenu();
+                    const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+                    const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
+                        title: _t("Are you sure?"),
+                        description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
+                            serverName: server,
+                        }, {
+                            b: serverName => <b>{ serverName }</b>,
+                        }),
+                        button: _t("Remove"),
+                        fixedWidth: false,
+                    }, "mx_NetworkDropdown_dialog");
 
-        if (!instance && includeAll) {
-            key = server;
-            name = server;
-        } else if (!instance) {
-            key = server + '_all';
-            name = 'Matrix';
-            icon = <img src={require("../../../../res/img/network-matrix.svg")} />;
-        } else {
-            key = server + '_inst_' + instance.instance_id;
-            const imgUrl = instance.icon ?
-                MatrixClientPeg.get().mxcUrlToHttp(instance.icon, 25, 25, 'crop', true) :
-                DEFAULT_ICON_URL;
-            icon = <img src={imgUrl} />;
-            name = instance.desc;
-        }
+                    const [ok] = await finished;
+                    if (!ok) return;
 
-        const clickHandler = handleClicks ? this.onMenuOptionClick.bind(this, server, instance, includeAll) : null;
+                    // delete from setting
+                    setUserDefinedServers(servers.filter(s => s !== server));
 
-        return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={clickHandler}>
-            {icon}
-            <span className="mx_NetworkDropdown_menu_network">{name}</span>
-        </div>;
-    }
+                    // the selected server is being removed, reset to our HS
+                    if (serverSelected === server) {
+                        onOptionChange(hsName, undefined);
+                    }
+                };
+                removeButton = <MenuItem onClick={onClick} label={_t("Remove server")} />;
+            }
 
-    render() {
-        let currentValue;
+            // ARIA: in actual fact the entire menu is one large radio group but for better screen reader support
+            // we use group to notate server wrongly.
+            return (
+                <MenuGroup label={server} className="mx_NetworkDropdown_server" key={server}>
+                    <div className="mx_NetworkDropdown_server_title">
+                        { server }
+                        { removeButton }
+                    </div>
+                    { subtitle }
 
-        let menu;
-        if (this.state.expanded) {
-            const menuOptions = this._getMenuOptions();
-            menu = <div className="mx_NetworkDropdown_menu">
-                {menuOptions}
-            </div>;
-            currentValue = <input type="text" className="mx_NetworkDropdown_networkoption"
-                ref={this.collectInputTextBox} onKeyUp={this.onInputKeyUp}
-                placeholder="matrix.org" // 'matrix.org' as an example of an HS name
-            />;
-        } else {
-            const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
-            currentValue = this._makeMenuOption(
-                this.state.selectedServer, instance, this.state.includeAllNetworks, false,
+                    <MenuItemRadio
+                        active={serverSelected && !selectedInstanceId}
+                        onClick={handlerFactory(server, undefined)}
+                        label={_t("Matrix")}
+                        className="mx_NetworkDropdown_server_network"
+                    >
+                        {_t("Matrix")}
+                    </MenuItemRadio>
+                    { entries }
+                </MenuGroup>
             );
+        });
+
+        const onClick = async () => {
+            closeMenu();
+            const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
+            const { finished } = Modal.createTrackedDialog("Network Dropdown", "Add a new server", TextInputDialog, {
+                title: _t("Add a new server"),
+                description: _t("Enter the name of a new server you want to explore."),
+                button: _t("Add"),
+                hasCancel: false,
+                placeholder: _t("Server name"),
+                validator: validServer,
+                fixedWidth: false,
+            }, "mx_NetworkDropdown_dialog");
+
+            const [ok, newServer] = await finished;
+            if (!ok) return;
+
+            if (!userDefinedServers.includes(newServer)) {
+                setUserDefinedServers([...userDefinedServers, newServer]);
+            }
+
+            onOptionChange(newServer); // change filter to the new server
+        };
+
+        const buttonRect = handle.current.getBoundingClientRect();
+        content = <ContextMenu {...inPlaceOf(buttonRect)} onFinished={closeMenu}>
+            <div className="mx_NetworkDropdown_menu">
+                {options}
+                <MenuItem className="mx_NetworkDropdown_server_add" label={undefined} onClick={onClick}>
+                    {_t("Add a new server...")}
+                </MenuItem>
+            </div>
+        </ContextMenu>;
+    } else {
+        let currentValue;
+        if (selectedInstanceId === ALL_ROOMS) {
+            currentValue = _t("All rooms");
+        } else if (selectedInstanceId) {
+            const instance = instanceForInstanceId(protocols, selectedInstanceId);
+            currentValue = _t("%(networkName)s rooms", {
+                networkName: instance.desc,
+            });
+        } else {
+            currentValue = _t("Matrix rooms");
         }
 
-        return <div className="mx_NetworkDropdown" ref={this.collectRoot}>
-            <div className="mx_NetworkDropdown_input mx_no_textinput" onClick={this.onInputClick}>
+        content = <ContextMenuButton
+            className="mx_NetworkDropdown_handle"
+            onClick={openMenu}
+            isExpanded={menuDisplayed}
+        >
+            <span>
                 {currentValue}
-                <span className="mx_NetworkDropdown_arrow" />
-                {menu}
-            </div>
-        </div>;
+            </span> <span className="mx_NetworkDropdown_handle_server">
+                ({selectedServerName})
+            </span>
+        </ContextMenuButton>;
     }
-}
+
+    return <div className="mx_NetworkDropdown" ref={handle}>
+        {content}
+    </div>;
+};
 
 NetworkDropdown.propTypes = {
     onOptionChange: PropTypes.func.isRequired,
     protocols: PropTypes.object,
-    // The room directory config. May have a 'servers' key that is a list of server names to include in the dropdown
-    config: PropTypes.object,
 };
 
-NetworkDropdown.defaultProps = {
-    protocols: {},
-    config: {},
-};
+export default NetworkDropdown;
diff --git a/src/components/views/elements/GeminiScrollbarWrapper.js b/src/components/views/elements/GeminiScrollbarWrapper.js
deleted file mode 100644
index 13eb14ecc3..0000000000
--- a/src/components/views/elements/GeminiScrollbarWrapper.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from 'react';
-import GeminiScrollbar from 'react-gemini-scrollbar';
-
-function GeminiScrollbarWrapper(props) {
-    const {wrappedRef, ...wrappedProps} = props;
-
-    // Enable forceGemini so that gemini is always enabled. This is
-    // to avoid future issues where a feature is implemented without
-    // doing QA on every OS/browser combination.
-    //
-    // By default GeminiScrollbar allows native scrollbars to be used
-    // on macOS. Use forceGemini to enable Gemini's non-native
-    // scrollbars on all OSs.
-    return <GeminiScrollbar ref={wrappedRef} forceGemini={true} {...wrappedProps}>
-        { props.children }
-    </GeminiScrollbar>;
-}
-export default GeminiScrollbarWrapper;
-
diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js
index e659352b74..f70c769ad7 100644
--- a/src/components/views/groups/GroupMemberInfo.js
+++ b/src/components/views/groups/GroupMemberInfo.js
@@ -27,6 +27,7 @@ import { GroupMemberType } from '../../../groups';
 import GroupStore from '../../../stores/GroupStore';
 import AccessibleButton from '../elements/AccessibleButton';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 
 export default createReactClass({
     displayName: 'GroupMemberInfo',
@@ -182,10 +183,9 @@ export default createReactClass({
             this.props.groupMember.displayname || this.props.groupMember.userId
         );
 
-        const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper');
         return (
             <div className="mx_MemberInfo" role="tabpanel">
-                <GeminiScrollbarWrapper autoshow={true}>
+                <AutoHideScrollbar>
                     <AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
                         <img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
                     </AccessibleButton>
@@ -199,7 +199,7 @@ export default createReactClass({
                     </div>
 
                     { adminTools }
-                </GeminiScrollbarWrapper>
+                </AutoHideScrollbar>
             </div>
         );
     },
diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js
index 05af70b266..2853e70afa 100644
--- a/src/components/views/groups/GroupMemberList.js
+++ b/src/components/views/groups/GroupMemberList.js
@@ -26,6 +26,7 @@ import { showGroupInviteDialog } from '../../../GroupAddressPicker';
 import AccessibleButton from '../elements/AccessibleButton';
 import TintableSvg from '../elements/TintableSvg';
 import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
+import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 
 const INITIAL_LOAD_NUM_MEMBERS = 30;
 
@@ -172,7 +173,6 @@ export default createReactClass({
     },
 
     render: function() {
-        const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
         if (this.state.fetching || this.state.fetchingInvitedMembers) {
             const Spinner = sdk.getComponent("elements.Spinner");
             return (<div className="mx_MemberList">
@@ -225,10 +225,10 @@ export default createReactClass({
         return (
             <div className="mx_MemberList" role="tabpanel">
                 { inviteButton }
-                <GeminiScrollbarWrapper autoshow={true}>
+                <AutoHideScrollbar>
                     { joined }
                     { invited }
-                </GeminiScrollbarWrapper>
+                </AutoHideScrollbar>
                 { inputBox }
             </div>
         );
diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js
index 7b9f43f15f..91d84be4d1 100644
--- a/src/components/views/groups/GroupRoomInfo.js
+++ b/src/components/views/groups/GroupRoomInfo.js
@@ -24,6 +24,7 @@ import * as sdk from '../../../index';
 import { _t } from '../../../languageHandler';
 import GroupStore from '../../../stores/GroupStore';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 
 export default createReactClass({
     displayName: 'GroupRoomInfo',
@@ -153,7 +154,6 @@ export default createReactClass({
     render: function() {
         const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
         const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
-        const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
         if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) {
             const Spinner = sdk.getComponent("elements.Spinner");
             return <div className="mx_MemberInfo">
@@ -216,7 +216,7 @@ export default createReactClass({
         const groupRoomName = this.state.groupRoom.displayname;
         return (
             <div className="mx_MemberInfo" role="tabpanel">
-                <GeminiScrollbarWrapper autoshow={true}>
+                <AutoHideScrollbar>
                     <AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
                         <img src={require("../../../../res/img/cancel.svg")} width="18" height="18" className="mx_filterFlipColor" />
                     </AccessibleButton>
@@ -231,7 +231,7 @@ export default createReactClass({
                     </div>
 
                     { adminTools }
-                </GeminiScrollbarWrapper>
+                </AutoHideScrollbar>
             </div>
         );
     },
diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js
index 5fd8c9f31d..dee304e1f6 100644
--- a/src/components/views/groups/GroupRoomList.js
+++ b/src/components/views/groups/GroupRoomList.js
@@ -22,6 +22,7 @@ import PropTypes from 'prop-types';
 import { showGroupAddRoomDialog } from '../../../GroupAddressPicker';
 import AccessibleButton from '../elements/AccessibleButton';
 import TintableSvg from '../elements/TintableSvg';
+import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
 
 const INITIAL_LOAD_NUM_ROOMS = 30;
 
@@ -150,17 +151,16 @@ export default createReactClass({
                     placeholder={_t('Filter community rooms')} autoComplete="off" />
         );
 
-        const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
         const TruncatedList = sdk.getComponent("elements.TruncatedList");
         return (
             <div className="mx_GroupRoomList" role="tabpanel">
                 { inviteButton }
-                <GeminiScrollbarWrapper autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
+                <AutoHideScrollbar className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
                     <TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
                             createOverflowElement={this._createOverflowTile}>
                         { this.makeGroupRoomTiles(this.state.searchQuery) }
                     </TruncatedList>
-                </GeminiScrollbarWrapper>
+                </AutoHideScrollbar>
                 { inputBox }
             </div>
         );
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js
index f8e2151c4f..857a80c34a 100644
--- a/src/components/views/room_settings/AliasSettings.js
+++ b/src/components/views/room_settings/AliasSettings.js
@@ -236,8 +236,7 @@ export default class AliasSettings extends React.Component {
         // TODO: In future, we should probably be making sure that the alias actually belongs
         // to this room. See https://github.com/vector-im/riot-web/issues/7353
         MatrixClientPeg.get().deleteAlias(alias).then(() => {
-            const localAliases = this.state.localAliases.slice();
-            localAliases.splice(index, 1);
+            const localAliases = this.state.localAliases.filter(a => a !== alias);
             this.setState({localAliases});
 
             if (this.state.canonicalAlias === alias) {
diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js
index dabf04ab99..147f3c0af8 100644
--- a/src/components/views/rooms/BasicMessageComposer.js
+++ b/src/components/views/rooms/BasicMessageComposer.js
@@ -370,6 +370,16 @@ export default class BasicMessageEditor extends React.Component {
         } else if (modKey && event.key === Key.GREATER_THAN) {
             this._onFormatAction("quote");
             handled = true;
+        // redo
+        } else if ((!IS_MAC && modKey && event.key === Key.Y) ||
+                  (IS_MAC && modKey && event.shiftKey && event.key === Key.Z)) {
+            if (this.historyManager.canRedo()) {
+                const {parts, caret} = this.historyManager.redo();
+                // pass matching inputType so historyManager doesn't push echo
+                // when invoked from rerender callback.
+                model.reset(parts, caret, "historyRedo");
+            }
+            handled = true;
         // undo
         } else if (modKey && event.key === Key.Z) {
             if (this.historyManager.canUndo()) {
@@ -379,15 +389,6 @@ export default class BasicMessageEditor extends React.Component {
                 model.reset(parts, caret, "historyUndo");
             }
             handled = true;
-        // redo
-        } else if (modKey && event.key === Key.Y) {
-            if (this.historyManager.canRedo()) {
-                const {parts, caret} = this.historyManager.redo();
-                // pass matching inputType so historyManager doesn't push echo
-                // when invoked from rerender callback.
-                model.reset(parts, caret, "historyRedo");
-            }
-            handled = true;
         // insert newline on Shift+Enter
         } else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
             this._insertText("\n");
diff --git a/src/components/views/rooms/JumpToBottomButton.js b/src/components/views/rooms/JumpToBottomButton.js
index 487071855f..d3305f498a 100644
--- a/src/components/views/rooms/JumpToBottomButton.js
+++ b/src/components/views/rooms/JumpToBottomButton.js
@@ -24,7 +24,7 @@ export default (props) => {
     }
     return (<div className="mx_JumpToBottomButton">
         <AccessibleButton className="mx_JumpToBottomButton_scrollDown"
-            title={_t("Scroll to bottom of page")}
+            title={_t("Scroll to most recent messages")}
             onClick={props.onScrollToBottomClick}>
         </AccessibleButton>
         { badge }
diff --git a/src/components/views/rooms/SearchableEntityList.js b/src/components/views/rooms/SearchableEntityList.js
deleted file mode 100644
index 807ddbf729..0000000000
--- a/src/components/views/rooms/SearchableEntityList.js
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import createReactClass from 'create-react-class';
-import * as sdk from "../../../index";
-import { _t } from '../../../languageHandler';
-
-// A list capable of displaying entities which conform to the SearchableEntity
-// interface which is an object containing getJsx(): Jsx and matches(query: string): boolean
-const SearchableEntityList = createReactClass({
-    displayName: 'SearchableEntityList',
-
-    propTypes: {
-        emptyQueryShowsAll: PropTypes.bool,
-        showInputBox: PropTypes.bool,
-        onQueryChanged: PropTypes.func, // fn(inputText)
-        onSubmit: PropTypes.func, // fn(inputText)
-        entities: PropTypes.array,
-        truncateAt: PropTypes.number,
-    },
-
-    getDefaultProps: function() {
-        return {
-            showInputBox: true,
-            entities: [],
-            emptyQueryShowsAll: false,
-            onSubmit: function() {},
-            onQueryChanged: function(input) {},
-        };
-    },
-
-    getInitialState: function() {
-        return {
-            query: "",
-            focused: false,
-            truncateAt: this.props.truncateAt,
-            results: this.getSearchResults("", this.props.entities),
-        };
-    },
-
-    componentWillReceiveProps: function(newProps) {
-        // recalculate the search results in case we got new entities
-        this.setState({
-            results: this.getSearchResults(this.state.query, newProps.entities),
-        });
-    },
-
-    componentWillUnmount: function() {
-        // pretend the query box was blanked out else filters could still be
-        // applied to other components which rely on onQueryChanged.
-        this.props.onQueryChanged("");
-    },
-
-    /**
-     * Public-facing method to set the input query text to the given input.
-     * @param {string} input
-     */
-    setQuery: function(input) {
-        this.setState({
-            query: input,
-            results: this.getSearchResults(input, this.props.entities),
-        });
-    },
-
-    onQueryChanged: function(ev) {
-        const q = ev.target.value;
-        this.setState({
-            query: q,
-            // reset truncation if they back out the entire text
-            truncateAt: (q.length === 0 ? this.props.truncateAt : this.state.truncateAt),
-            results: this.getSearchResults(q, this.props.entities),
-        }, () => {
-            // invoke the callback AFTER we've flushed the new state. We need to
-            // do this because onQueryChanged can result in new props being passed
-            // to this component, which will then try to recalculate the search
-            // list. If we do this without flushing, we'll recalc with the last
-            // search term and not the current one!
-            this.props.onQueryChanged(q);
-        });
-    },
-
-    onQuerySubmit: function(ev) {
-        ev.preventDefault();
-        this.props.onSubmit(this.state.query);
-    },
-
-    getSearchResults: function(query, entities) {
-        if (!query || query.length === 0) {
-            return this.props.emptyQueryShowsAll ? entities : [];
-        }
-        return entities.filter(function(e) {
-            return e.matches(query);
-        });
-    },
-
-    _showAll: function() {
-        this.setState({
-            truncateAt: -1,
-        });
-    },
-
-    _createOverflowEntity: function(overflowCount, totalCount) {
-        const EntityTile = sdk.getComponent("rooms.EntityTile");
-        const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
-        const text = _t("and %(count)s others...", { count: overflowCount });
-        return (
-            <EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
-                <BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
-            } name={text} presenceState="online" suppressOnHover={true}
-            onClick={this._showAll} />
-        );
-    },
-
-    render: function() {
-        let inputBox;
-
-        if (this.props.showInputBox) {
-            inputBox = (
-                <form onSubmit={this.onQuerySubmit} autoComplete="off">
-                    <input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text"
-                        onChange={this.onQueryChanged} value={this.state.query}
-                        onFocus= {() => { this.setState({ focused: true }); }}
-                        onBlur= {() => { this.setState({ focused: false }); }}
-                        placeholder={_t("Search")} />
-                </form>
-            );
-        }
-
-        let list;
-        if (this.state.results.length > 1 || this.state.focused) {
-            if (this.props.truncateAt) { // caller wants list truncated
-                const TruncatedList = sdk.getComponent("elements.TruncatedList");
-                list = (
-                    <TruncatedList className="mx_SearchableEntityList_list"
-                            truncateAt={this.state.truncateAt} // use state truncation as it may be expanded
-                            createOverflowElement={this._createOverflowEntity}>
-                        { this.state.results.map((entity) => {
-                            return entity.getJsx();
-                        }) }
-                    </TruncatedList>
-                );
-            } else {
-                list = (
-                    <div className="mx_SearchableEntityList_list">
-                        { this.state.results.map((entity) => {
-                            return entity.getJsx();
-                        }) }
-                    </div>
-                );
-            }
-            const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
-            list = (
-                <GeminiScrollbarWrapper autoshow={true}
-                                 className="mx_SearchableEntityList_listWrapper">
-                    { list }
-                </GeminiScrollbarWrapper>
-            );
-        }
-
-        return (
-            <div className={"mx_SearchableEntityList " + (list ? "mx_SearchableEntityList_expanded" : "")}>
-                { inputBox }
-                { list }
-                { list ? <div className="mx_SearchableEntityList_hrWrapper"><hr /></div> : '' }
-            </div>
-        );
-    },
-});
-
-export default SearchableEntityList;
diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js
index 73be2bad9f..27fdb2cb56 100644
--- a/src/components/views/settings/KeyBackupPanel.js
+++ b/src/components/views/settings/KeyBackupPanel.js
@@ -277,25 +277,25 @@ export default class KeyBackupPanel extends React.PureComponent {
                         "Backup has an <validity>invalid</validity> signature from this session",
                         {}, { validity },
                     );
-                } else if (sig.valid && sig.device.isVerified()) {
+                } else if (sig.valid && sig.deviceTrust.isVerified()) {
                     sigStatus = _t(
                         "Backup has a <validity>valid</validity> signature from " +
                         "<verify>verified</verify> session <device></device>",
                         {}, { validity, verify, device },
                     );
-                } else if (sig.valid && !sig.device.isVerified()) {
+                } else if (sig.valid && !sig.deviceTrust.isVerified()) {
                     sigStatus = _t(
                         "Backup has a <validity>valid</validity> signature from " +
                         "<verify>unverified</verify> session <device></device>",
                         {}, { validity, verify, device },
                     );
-                } else if (!sig.valid && sig.device.isVerified()) {
+                } else if (!sig.valid && sig.deviceTrust.isVerified()) {
                     sigStatus = _t(
                         "Backup has an <validity>invalid</validity> signature from " +
                         "<verify>verified</verify> session <device></device>",
                         {}, { validity, verify, device },
                     );
-                } else if (!sig.valid && !sig.device.isVerified()) {
+                } else if (!sig.valid && !sig.deviceTrust.isVerified()) {
                     sigStatus = _t(
                         "Backup has an <validity>invalid</validity> signature from " +
                         "<verify>unverified</verify> session <device></device>",
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d9b8f4f0bd..f8c8ad0200 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -960,7 +960,7 @@
     "Encrypted by a deleted session": "Encrypted by a deleted session",
     "Please select the destination room for this message": "Please select the destination room for this message",
     "Invite only": "Invite only",
-    "Scroll to bottom of page": "Scroll to bottom of page",
+    "Scroll to most recent messages": "Scroll to most recent messages",
     "Close preview": "Close preview",
     "device id: ": "device id: ",
     "Disinvite": "Disinvite",
@@ -1449,6 +1449,20 @@
     "And %(count)s more...|other": "And %(count)s more...",
     "ex. @bob:example.com": "ex. @bob:example.com",
     "Add User": "Add User",
+    "Enter a server name": "Enter a server name",
+    "Looks good": "Looks good",
+    "Can't find this server or its room list": "Can't find this server or its room list",
+    "All rooms": "All rooms",
+    "Your server": "Your server",
+    "Are you sure you want to remove <b>%(serverName)s</b>": "Are you sure you want to remove <b>%(serverName)s</b>",
+    "Remove server": "Remove server",
+    "Matrix": "Matrix",
+    "Add a new server": "Add a new server",
+    "Enter the name of a new server you want to explore.": "Enter the name of a new server you want to explore.",
+    "Server name": "Server name",
+    "Add a new server...": "Add a new server...",
+    "%(networkName)s rooms": "%(networkName)s rooms",
+    "Matrix rooms": "Matrix rooms",
     "Matrix ID": "Matrix ID",
     "Matrix Room ID": "Matrix Room ID",
     "email address": "email address",
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index abcfd21902..461761dfa2 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -330,6 +330,10 @@ export const SETTINGS = {
         supportedLevels: ['account'],
         default: [],
     },
+    "room_directory_servers": {
+        supportedLevels: ['account'],
+        default: [],
+    },
     "integrationProvisioning": {
         supportedLevels: ['account'],
         default: true,
diff --git a/yarn.lock b/yarn.lock
index 705b02e3e4..744cfbc563 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3902,10 +3902,6 @@ fuse.js@^2.2.0:
   resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-2.7.4.tgz#96e420fde7ef011ac49c258a621314fe576536f9"
   integrity sha1-luQg/efvARrEnCWKYhMU/ldlNvk=
 
-"gemini-scrollbar@github:matrix-org/gemini-scrollbar#91e1e566", gemini-scrollbar@matrix-org/gemini-scrollbar#91e1e566:
-  version "1.4.3"
-  resolved "https://codeload.github.com/matrix-org/gemini-scrollbar/tar.gz/91e1e566fa33324188f278801baf4a79f9f554ab"
-
 gensync@^1.0.0-beta.1:
   version "1.0.0-beta.1"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
@@ -5689,8 +5685,8 @@ mathml-tag-names@^2.0.1:
   integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
 
 "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
-  version "5.1.0"
-  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/86304fd037ac43a493000fa42f393eaafc0480ac"
+  version "5.1.1"
+  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b2e154377a4268441a3b27b183dd7f7018187035"
   dependencies:
     "@babel/runtime" "^7.8.3"
     another-json "^0.2.0"
@@ -6963,12 +6959,6 @@ react-focus-lock@^2.2.1:
     use-callback-ref "^1.2.1"
     use-sidecar "^1.0.1"
 
-"react-gemini-scrollbar@github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594":
-  version "2.1.5"
-  resolved "https://codeload.github.com/matrix-org/react-gemini-scrollbar/tar.gz/9cf17f63b7c0b0ec5f31df27da0f82f7238dc594"
-  dependencies:
-    gemini-scrollbar matrix-org/gemini-scrollbar#91e1e566
-
 react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
   version "16.13.0"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527"