diff --git a/.eslintrc.js b/.eslintrc.js
index fc82e75ce2..bc2a142c2d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -19,7 +19,7 @@ module.exports = {
     },
 
     overrides: [{
-        "files": ["src/**/*.{ts, tsx}"],
+        "files": ["src/**/*.{ts,tsx}"],
         "extends": ["matrix-org/ts"],
         "rules": {
             // We disable this while we're transitioning
diff --git a/package.json b/package.json
index fc24a79b48..1f7f10a04c 100644
--- a/package.json
+++ b/package.json
@@ -163,9 +163,7 @@
     "stylelint-config-standard": "^18.3.0",
     "stylelint-scss": "^3.18.0",
     "typescript": "^3.9.7",
-    "walk": "^2.3.14",
-    "webpack": "^4.43.0",
-    "webpack-cli": "^3.3.12"
+    "walk": "^2.3.14"
   },
   "jest": {
     "testMatch": [
diff --git a/res/css/_components.scss b/res/css/_components.scss
index 5145133127..24d2ffa2b0 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -61,7 +61,9 @@
 @import "./views/dialogs/_BugReportDialog.scss";
 @import "./views/dialogs/_ChangelogDialog.scss";
 @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
+@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss";
 @import "./views/dialogs/_ConfirmUserActionDialog.scss";
+@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
 @import "./views/dialogs/_CreateGroupDialog.scss";
 @import "./views/dialogs/_CreateRoomDialog.scss";
 @import "./views/dialogs/_DeactivateAccountDialog.scss";
@@ -106,6 +108,7 @@
 @import "./views/elements/_FormButton.scss";
 @import "./views/elements/_IconButton.scss";
 @import "./views/elements/_ImageView.scss";
+@import "./views/elements/_InfoTooltip.scss";
 @import "./views/elements/_InlineSpinner.scss";
 @import "./views/elements/_ManageIntegsButton.scss";
 @import "./views/elements/_PowerSelector.scss";
diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss
index 6c85341aaf..cdca1f0764 100644
--- a/res/css/structures/_TagPanel.scss
+++ b/res/css/structures/_TagPanel.scss
@@ -30,25 +30,6 @@ limitations under the License.
     cursor: pointer;
 }
 
-.mx_TagPanel .mx_TagPanel_clearButton_container {
-    /* Constant height within flex mx_TagPanel */
-    height: 70px;
-    width: 56px;
-
-    flex: none;
-
-    justify-content: center;
-    align-items: flex-start;
-
-    display: none;
-}
-
-.mx_TagPanel .mx_TagPanel_clearButton object {
-    /* Same as .mx_SearchBox padding-top */
-    margin-top: 24px;
-    pointer-events: none;
-}
-
 .mx_TagPanel .mx_TagPanel_divider {
     height: 0px;
     width: 90%;
@@ -76,12 +57,57 @@ limitations under the License.
     // opacity: 0.5;
     position: relative;
 }
+
+.mx_TagPanel .mx_TagTile.mx_TagTile_prototype {
+    padding: 3px;
+}
+
 .mx_TagPanel .mx_TagTile:focus,
 .mx_TagPanel .mx_TagTile:hover,
 .mx_TagPanel .mx_TagTile.mx_TagTile_selected {
     // opacity: 1;
 }
 
+.mx_TagPanel .mx_TagTile.mx_TagTile_selected_prototype {
+    background-color: $primary-bg-color;
+    border-radius: 6px;
+}
+
+.mx_TagTile_selected_prototype {
+    .mx_TagTile_homeIcon::before {
+        background-color: $primary-fg-color; // dark-on-light
+    }
+}
+
+.mx_TagTile:not(.mx_TagTile_selected_prototype) .mx_TagTile_homeIcon {
+    background-color: $roomheader-addroom-bg-color;
+    border-radius: 48px;
+
+    &::before {
+        background-color: $roomheader-addroom-fg-color;
+    }
+}
+
+.mx_TagTile_homeIcon {
+    width: 32px;
+    height: 32px;
+    position: relative;
+
+    &::before {
+        mask-image: url('$(res)/img/element-icons/home.svg');
+        mask-position: center;
+        mask-repeat: no-repeat;
+        mask-size: 21px;
+        content: '';
+        display: inline-block;
+        width: 32px;
+        height: 32px;
+        position: absolute;
+        top: calc(50% - 16px);
+        left: calc(50% - 16px);
+    }
+}
+
 .mx_TagPanel .mx_TagTile_plus {
     margin-bottom: 12px;
     height: 32px;
@@ -116,10 +142,6 @@ limitations under the License.
     border-radius: 0 3px 3px 0;
 }
 
-.mx_TagPanel .mx_TagTile.mx_TagTile_large.mx_TagTile_selected::before {
-    left: -10px;
-}
-
 .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
     filter: none;
 }
diff --git a/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss
new file mode 100644
index 0000000000..beae03f00f
--- /dev/null
+++ b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss
@@ -0,0 +1,88 @@
+/*
+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.
+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_CommunityPrototypeInviteDialog {
+    &.mx_Dialog_fixedWidth {
+        width: 360px;
+    }
+
+    .mx_Dialog_content {
+        margin-bottom: 0;
+
+        .mx_CommunityPrototypeInviteDialog_people {
+            position: relative;
+            margin-bottom: 4px;
+
+            .mx_AccessibleButton {
+                display: inline-block;
+                background-color: $focus-bg-color; // XXX: Abuse of variables
+                border-radius: 4px;
+                padding: 3px 5px;
+                font-size: $font-12px;
+                float: right;
+            }
+        }
+
+        .mx_CommunityPrototypeInviteDialog_morePeople {
+            margin-top: 8px;
+        }
+
+        .mx_CommunityPrototypeInviteDialog_person {
+            position: relative;
+            margin-top: 4px;
+
+            & > * {
+                vertical-align: middle;
+            }
+
+            .mx_Checkbox {
+                position: absolute;
+                right: 0;
+                top: calc(50% - 8px); // checkbox is 16px high
+                width: 16px; // to force a square
+            }
+
+            .mx_CommunityPrototypeInviteDialog_personIdentifiers {
+                display: inline-block;
+
+                & > * {
+                    display: block;
+                }
+
+                .mx_CommunityPrototypeInviteDialog_personName {
+                    font-weight: 600;
+                    font-size: $font-14px;
+                    color: $primary-fg-color;
+                    margin-left: 7px;
+                }
+
+                .mx_CommunityPrototypeInviteDialog_personId {
+                    font-size: $font-12px;
+                    color: $muted-fg-color;
+                    margin-left: 7px;
+                }
+            }
+        }
+
+        .mx_CommunityPrototypeInviteDialog_primaryButton {
+            display: block;
+            font-size: $font-13px;
+            line-height: 20px;
+            height: 20px;
+            margin-top: 24px;
+        }
+    }
+}
diff --git a/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss b/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss
new file mode 100644
index 0000000000..81babc4c38
--- /dev/null
+++ b/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss
@@ -0,0 +1,102 @@
+/*
+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.
+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_CreateCommunityPrototypeDialog {
+    .mx_Dialog_content {
+        display: flex;
+        flex-direction: row;
+        margin-bottom: 12px;
+
+        .mx_CreateCommunityPrototypeDialog_colName {
+            flex-basis: 66.66%;
+            padding-right: 100px;
+
+            .mx_Field input {
+                font-size: $font-16px;
+                line-height: $font-20px;
+            }
+
+            .mx_CreateCommunityPrototypeDialog_subtext {
+                display: block;
+                color: $muted-fg-color;
+                margin-bottom: 16px;
+
+                &:last-child {
+                    margin-top: 16px;
+                }
+
+                &.mx_CreateCommunityPrototypeDialog_subtext_error {
+                    color: $warning-color;
+                }
+            }
+
+            .mx_CreateCommunityPrototypeDialog_communityId {
+                position: relative;
+
+                .mx_InfoTooltip {
+                    float: right;
+                }
+            }
+
+            .mx_AccessibleButton {
+                display: block;
+                height: 32px;
+                font-size: $font-16px;
+                line-height: 32px;
+            }
+        }
+
+        .mx_CreateCommunityPrototypeDialog_colAvatar {
+            flex-basis: 33.33%;
+
+            .mx_CreateCommunityPrototypeDialog_avatarContainer {
+                margin-top: 12px;
+                margin-bottom: 20px;
+
+                .mx_CreateCommunityPrototypeDialog_avatar,
+                .mx_CreateCommunityPrototypeDialog_placeholderAvatar {
+                    width: 96px;
+                    height: 96px;
+                    border-radius: 96px;
+                }
+
+                .mx_CreateCommunityPrototypeDialog_placeholderAvatar {
+                    background-color: #368bd6; // hardcoded for both themes
+
+                    &::before {
+                        display: inline-block;
+                        background-color: #fff; // hardcoded because the background is
+                        mask-repeat: no-repeat;
+                        mask-size: 96px;
+                        width: 96px;
+                        height: 96px;
+                        mask-position: center;
+                        content: '';
+                        vertical-align: middle;
+                        mask-image: url('$(res)/img/element-icons/add-photo.svg');
+                    }
+                }
+            }
+
+            .mx_CreateCommunityPrototypeDialog_tip {
+                & > b, & > span {
+                    display: block;
+                    color: $muted-fg-color;
+                }
+            }
+        }
+    }
+}
diff --git a/res/css/views/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss
new file mode 100644
index 0000000000..5858a60629
--- /dev/null
+++ b/res/css/views/elements/_InfoTooltip.scss
@@ -0,0 +1,34 @@
+/*
+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.
+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_InfoTooltip_icon {
+    width: 16px;
+    height: 16px;
+    display: inline-block;
+}
+
+.mx_InfoTooltip_icon::before {
+    display: inline-block;
+    background-color: $muted-fg-color;
+    mask-repeat: no-repeat;
+    mask-size: 16px;
+    width: 16px;
+    height: 16px;
+    mask-position: center;
+    content: '';
+    vertical-align: middle;
+    mask-image: url('$(res)/img/element-icons/info.svg');
+}
diff --git a/res/img/element-icons/add-photo.svg b/res/img/element-icons/add-photo.svg
new file mode 100644
index 0000000000..bde5253bea
--- /dev/null
+++ b/res/img/element-icons/add-photo.svg
@@ -0,0 +1,5 @@
+<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7988 34.9062C37.077 33.5217 38.2978 32.5 39.7396 32.5H44.2604C45.7022 32.5 46.923 33.5217 47.2012 34.9062C47.2429 35.1137 47.3232 35.3141 47.4627 35.4731L48.0649 36.1595C48.2548 36.3759 48.5287 36.5 48.8166 36.5H52C53.1046 36.5 54 37.3954 54 38.5V49.5C54 50.6046 53.1046 51.5 52 51.5H32C30.8954 51.5 30 50.6046 30 49.5V38.5C30 37.3954 30.8954 36.5 32 36.5H35.1834C35.4713 36.5 35.7452 36.3759 35.9351 36.1595L36.5373 35.4731C36.6768 35.3141 36.7571 35.1137 36.7988 34.9062ZM42 47.5C44.2091 47.5 46 45.7091 46 43.5C46 41.2909 44.2091 39.5 42 39.5C39.7909 39.5 38 41.2909 38 43.5C38 45.7091 39.7909 47.5 42 47.5Z" fill="white"/>
+<rect x="32" y="35" width="3" height="1" rx="0.5" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M59.75 27C59.75 26.5858 59.4142 26.25 59 26.25C58.5858 26.25 58.25 26.5858 58.25 27V31.25L54 31.25C53.5858 31.25 53.25 31.5858 53.25 32C53.25 32.4142 53.5858 32.75 54 32.75L58.25 32.75V37C58.25 37.4142 58.5858 37.75 59 37.75C59.4142 37.75 59.75 37.4142 59.75 37V32.75L64 32.75C64.4142 32.75 64.75 32.4142 64.75 32C64.75 31.5858 64.4142 31.25 64 31.25L59.75 31.25V27Z" fill="white"/>
+</svg>
diff --git a/res/img/element-icons/home.svg b/res/img/element-icons/home.svg
new file mode 100644
index 0000000000..a6c15456ff
--- /dev/null
+++ b/res/img/element-icons/home.svg
@@ -0,0 +1,3 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.33301 7.28322V14.9493C2.33301 16.0735 3.25744 16.9776 4.38152 16.9659C4.90089 16.9605 5.44431 16.9567 6 16.9543V11.5C6 10.6716 6.67157 10 7.5 10H10.5C11.3284 10 12 10.6716 12 11.5V16.9662C12.6022 16.9703 13.1579 16.9748 13.6449 16.9791C14.7592 16.989 15.6663 16.0899 15.6663 14.9756V7.28178C15.6663 6.89062 15.4946 6.52064 15.1965 6.2673L9.97115 1.82572C9.411 1.3496 8.58834 1.3496 8.0282 1.82572L2.80281 6.2673C2.50477 6.52064 2.33301 6.89206 2.33301 7.28322Z" fill="#737D8C"/>
+</svg>
diff --git a/res/img/element-icons/info.svg b/res/img/element-icons/info.svg
new file mode 100644
index 0000000000..b5769074ab
--- /dev/null
+++ b/res/img/element-icons/info.svg
@@ -0,0 +1,4 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="10" cy="10" r="9.5" stroke="#787878"/>
+<path d="M9.79248 14H11.2065V8H9.79248V14ZM10.5034 7.14844C10.9526 7.14844 11.3198 6.80469 11.3198 6.38281C11.3198 5.95703 10.9526 5.61328 10.5034 5.61328C10.0503 5.61328 9.68311 5.95703 9.68311 6.38281C9.68311 6.80469 10.0503 7.14844 10.5034 7.14844Z" fill="#787878"/>
+</svg>
diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx
index 6f55a75d0c..eb8fff0eb1 100644
--- a/src/ContentMessages.tsx
+++ b/src/ContentMessages.tsx
@@ -70,6 +70,7 @@ interface IContent {
 
 interface IThumbnail {
     info: {
+        // eslint-disable-next-line camelcase
         thumbnail_info: {
             w: number;
             h: number;
@@ -104,7 +105,12 @@ interface IAbortablePromise<T> extends Promise<T> {
  * @return {Promise} A promise that resolves with an object with an info key
  *  and a thumbnail key.
  */
-function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise<IThumbnail> {
+function createThumbnail(
+    element: ThumbnailableElement,
+    inputWidth: number,
+    inputHeight: number,
+    mimeType: string,
+): Promise<IThumbnail> {
     return new Promise((resolve) => {
         let targetWidth = inputWidth;
         let targetHeight = inputHeight;
@@ -437,11 +443,13 @@ export default class ContentMessages {
         for (let i = 0; i < okFiles.length; ++i) {
             const file = okFiles[i];
             if (!uploadAll) {
-                const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, {
-                    file,
-                    currentIndex: i,
-                    totalFiles: okFiles.length,
-                });
+                const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation',
+                    '', UploadConfirmDialog, {
+                        file,
+                        currentIndex: i,
+                        totalFiles: okFiles.length,
+                    },
+                );
                 const [shouldContinue, shouldUploadAll] = await finished;
                 if (!shouldContinue) break;
                 if (shouldUploadAll) {
diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js
index da09a436e9..0353bfc5ae 100644
--- a/src/CrossSigningManager.js
+++ b/src/CrossSigningManager.js
@@ -69,19 +69,19 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
     if (keyInfoEntries.length > 1) {
         throw new Error("Multiple storage key requests not implemented");
     }
-    const [name, info] = keyInfoEntries[0];
+    const [keyId, keyInfo] = keyInfoEntries[0];
 
     // Check the in-memory cache
-    if (isCachingAllowed() && secretStorageKeys[name]) {
-        return [name, secretStorageKeys[name]];
+    if (isCachingAllowed() && secretStorageKeys[keyId]) {
+        return [keyId, secretStorageKeys[keyId]];
     }
 
     const inputToKey = async ({ passphrase, recoveryKey }) => {
         if (passphrase) {
             return deriveKey(
                 passphrase,
-                info.passphrase.salt,
-                info.passphrase.iterations,
+                keyInfo.passphrase.salt,
+                keyInfo.passphrase.iterations,
             );
         } else {
             return decodeRecoveryKey(recoveryKey);
@@ -93,10 +93,10 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
         AccessSecretStorageDialog,
         /* props= */
         {
-            keyInfo: info,
+            keyInfo,
             checkPrivateKey: async (input) => {
                 const key = await inputToKey(input);
-                return await MatrixClientPeg.get().checkSecretStorageKey(key, info);
+                return await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo);
             },
         },
         /* className= */ null,
@@ -118,11 +118,15 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
     const key = await inputToKey(input);
 
     // Save to cache to avoid future prompts in the current session
-    if (isCachingAllowed()) {
-        secretStorageKeys[name] = key;
-    }
+    cacheSecretStorageKey(keyId, key);
 
-    return [name, key];
+    return [keyId, key];
+}
+
+function cacheSecretStorageKey(keyId, key) {
+    if (isCachingAllowed()) {
+        secretStorageKeys[keyId] = key;
+    }
 }
 
 const onSecretRequested = async function({
@@ -170,6 +174,7 @@ const onSecretRequested = async function({
 
 export const crossSigningCallbacks = {
     getSecretStorageKey,
+    cacheSecretStorageKey,
     onSecretRequested,
 };
 
@@ -218,7 +223,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
             const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
                 import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
                 {
-                    force: forceReset,
+                    forceReset,
                 },
                 null,
                 /* priority = */ false,
@@ -239,7 +244,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
             }
         } else {
             const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
-            await cli.bootstrapSecretStorage({
+            await cli.bootstrapCrossSigning({
                 authUploadDeviceSigningKeys: async (makeRequest) => {
                     const { finished } = Modal.createTrackedDialog(
                         'Cross-signing keys dialog', '', InteractiveAuthDialog,
@@ -254,7 +259,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
                         throw new Error("Cross-signing key upload auth canceled");
                     }
                 },
-                getBackupPassphrase: promptForBackupPassphrase,
+            });
+            await cli.bootstrapSecretStorage({
+                getKeyBackupPassphrase: promptForBackupPassphrase,
             });
         }
 
diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts
index 6b667ae54d..b05f0fcd68 100644
--- a/src/DeviceListener.ts
+++ b/src/DeviceListener.ts
@@ -207,9 +207,13 @@ export default class DeviceListener {
         // (we add a listener on sync to do once check after the initial sync is done)
         if (!cli.isInitialSyncComplete()) return;
 
+        // JRS: This will change again in the next PR which moves secret storage
+        // later in the process.
         const crossSigningReady = await cli.isCrossSigningReady();
+        const secretStorageReady = await cli.isSecretStorageReady();
+        const allSystemsReady = crossSigningReady && secretStorageReady;
 
-        if (this.dismissedThisDeviceToast || crossSigningReady) {
+        if (this.dismissedThisDeviceToast || allSystemsReady) {
             hideSetupEncryptionToast();
         } else if (this.shouldShowSetupEncryptionToast()) {
             // make sure our keys are finished downloading
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 5d33645bb7..bd314c2e5f 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -339,33 +339,9 @@ class HtmlHighlighter extends BaseHighlighter<string> {
     }
 }
 
-class TextHighlighter extends BaseHighlighter<React.ReactNode> {
-    private key = 0;
-
-    /* create a <span> node to hold the given content
-     *
-     * snippet: content of the span
-     * highlight: true to highlight as a search match
-     *
-     * returns a React node
-     */
-    protected processSnippet(snippet: string, highlight: boolean): React.ReactNode {
-        const key = this.key++;
-
-        let node = <span key={key} className={highlight ? this.highlightClass : null}>
-            { snippet }
-        </span>;
-
-        if (highlight && this.highlightLink) {
-            node = <a key={key} href={this.highlightLink}>{ node }</a>;
-        }
-
-        return node;
-    }
-}
-
 interface IContent {
     format?: string;
+    // eslint-disable-next-line camelcase
     formatted_body?: string;
     body: string;
 }
@@ -474,8 +450,13 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
     });
 
     return isDisplayedWithHtml ?
-        <span key="body" ref={opts.ref} className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
-        <span key="body" ref={opts.ref} className={className} dir="auto">{ strippedBody }</span>;
+        <span
+            key="body"
+            ref={opts.ref}
+            className={className}
+            dangerouslySetInnerHTML={{ __html: safeBody }}
+            dir="auto"
+        /> : <span key="body" ref={opts.ref} className={className} dir="auto">{ strippedBody }</span>;
 }
 
 /**
diff --git a/src/Modal.tsx b/src/Modal.tsx
index 82ed33b794..0a36813961 100644
--- a/src/Modal.tsx
+++ b/src/Modal.tsx
@@ -151,7 +151,7 @@ export class ModalManager {
         prom: Promise<React.ComponentType>,
         props?: IProps<T>,
         className?: string,
-        options?: IOptions<T>
+        options?: IOptions<T>,
     ) {
         const modal: IModal<T> = {
             onFinished: props ? props.onFinished : null,
@@ -182,7 +182,7 @@ export class ModalManager {
 
     private getCloseFn<T extends any[]>(
         modal: IModal<T>,
-        props: IProps<T>
+        props: IProps<T>,
     ): [IHandle<T>["close"], IHandle<T>["finished"]] {
         const deferred = defer<T>();
         return [async (...args: T) => {
@@ -264,7 +264,7 @@ export class ModalManager {
         className?: string,
         isPriorityModal = false,
         isStaticModal = false,
-        options: IOptions<T> = {}
+        options: IOptions<T> = {},
     ): IHandle<T> {
         const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, options);
         if (isPriorityModal) {
@@ -287,7 +287,7 @@ export class ModalManager {
     private appendDialogAsync<T extends any[]>(
         prom: Promise<React.ComponentType>,
         props?: IProps<T>,
-        className?: string
+        className?: string,
     ): IHandle<T> {
         const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, {});
 
diff --git a/src/RoomInvite.js b/src/RoomInvite.js
index 839d677069..420561ea41 100644
--- a/src/RoomInvite.js
+++ b/src/RoomInvite.js
@@ -23,6 +23,7 @@ import Modal from './Modal';
 import * as sdk from './';
 import { _t } from './languageHandler';
 import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
+import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
 
 /**
  * Invites multiple addresses to a room
@@ -56,6 +57,13 @@ export function showRoomInviteDialog(roomId) {
     );
 }
 
+export function showCommunityRoomInviteDialog(roomId, communityName) {
+    Modal.createTrackedDialog(
+        'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
+        /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
+    );
+}
+
 /**
  * Checks if the given MatrixEvent is a valid 3rd party user invite.
  * @param {MatrixEvent} event The event to check
@@ -77,7 +85,7 @@ export function isValid3pidInvite(event) {
 export function inviteUsersToRoom(roomId, userIds) {
     return inviteMultipleToRoom(roomId, userIds).then((result) => {
         const room = MatrixClientPeg.get().getRoom(roomId);
-        return _showAnyInviteErrors(result.states, room, result.inviter);
+        showAnyInviteErrors(result.states, room, result.inviter);
     }).catch((err) => {
         console.error(err.stack);
         const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@@ -88,7 +96,7 @@ export function inviteUsersToRoom(roomId, userIds) {
     });
 }
 
-function _showAnyInviteErrors(addrs, room, inviter) {
+export function showAnyInviteErrors(addrs, room, inviter) {
     // Show user any errors
     const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
     if (failedUsers.length === 1 && inviter.fatal) {
@@ -100,6 +108,7 @@ function _showAnyInviteErrors(addrs, room, inviter) {
             title: _t("Failed to invite users to the room:", {roomName: room.name}),
             description: inviter.getErrorText(failedUsers[0]),
         });
+        return false;
     } else {
         const errorList = [];
         for (const addr of failedUsers) {
@@ -118,8 +127,9 @@ function _showAnyInviteErrors(addrs, room, inviter) {
                 title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
                 description,
             });
+            return false;
         }
     }
 
-    return addrs;
+    return true;
 }
diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index d674634109..661ab74e6f 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -860,12 +860,12 @@ export const Commands = [
                                 _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session' +
                                     ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
                                     '"%(fingerprint)s". This could mean your communications are being intercepted!',
-                                    {
-                                        fprint,
-                                        userId,
-                                        deviceId,
-                                        fingerprint,
-                                    }));
+                                {
+                                    fprint,
+                                    userId,
+                                    deviceId,
+                                    fingerprint,
+                                }));
                         }
 
                         await cli.setDeviceVerified(userId, deviceId, true);
@@ -879,7 +879,7 @@ export const Commands = [
                                     {
                                         _t('The signing key you provided matches the signing key you received ' +
                                             'from %(userId)s\'s session %(deviceId)s. Session marked as verified.',
-                                            {userId, deviceId})
+                                        {userId, deviceId})
                                     }
                                 </p>
                             </div>,
diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx
index f527ab4a14..58d8124122 100644
--- a/src/accessibility/KeyboardShortcuts.tsx
+++ b/src/accessibility/KeyboardShortcuts.tsx
@@ -168,7 +168,7 @@ const shortcuts: Record<Categories, IShortcut[]> = {
                 key: Key.U,
             }],
             description: _td("Upload a file"),
-        }
+        },
     ],
 
     [Categories.ROOM_LIST]: [
diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx
index 5a650d4b6e..b1dbb56a01 100644
--- a/src/accessibility/RovingTabIndex.tsx
+++ b/src/accessibility/RovingTabIndex.tsx
@@ -190,7 +190,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
             ev.preventDefault();
             ev.stopPropagation();
         } else if (onKeyDown) {
-            return onKeyDown(ev, state);
+            return onKeyDown(ev, context.state);
         }
     }, [context.state, onKeyDown, handleHomeEnd]);
 
diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx
index 0e968461a8..cc2a1769c7 100644
--- a/src/accessibility/Toolbar.tsx
+++ b/src/accessibility/Toolbar.tsx
@@ -30,6 +30,7 @@ const Toolbar: React.FC<IProps> = ({children, ...props}) => {
         const target = ev.target as HTMLElement;
         let handled = true;
 
+        // HOME and END are handled by RovingTabIndexProvider
         switch (ev.key) {
             case Key.ARROW_UP:
             case Key.ARROW_DOWN:
@@ -47,8 +48,6 @@ const Toolbar: React.FC<IProps> = ({children, ...props}) => {
                 }
                 break;
 
-            // HOME and END are handled by RovingTabIndexProvider
-
             default:
                 handled = false;
         }
diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx
index abc5412100..49f57ca7b6 100644
--- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx
+++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx
@@ -20,7 +20,7 @@ import React from "react";
 
 import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton";
 
-interface IProps extends React.ComponentProps<typeof AccessibleTooltipButton>  {
+interface IProps extends React.ComponentProps<typeof AccessibleTooltipButton> {
     // whether or not the context menu is currently open
     isExpanded: boolean;
 }
diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx
index cc824fef22..2cb974d60e 100644
--- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx
+++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx
@@ -20,7 +20,8 @@ import AccessibleTooltipButton from "../../components/views/elements/AccessibleT
 import {useRovingTabIndex} from "../RovingTabIndex";
 import {Ref} from "./types";
 
-interface IProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButton>, "onFocus" | "inputRef" | "tabIndex"> {
+type ATBProps = React.ComponentProps<typeof AccessibleTooltipButton>;
+interface IProps extends Omit<ATBProps, "onFocus" | "inputRef" | "tabIndex"> {
     inputRef?: Ref;
 }
 
diff --git a/src/accessibility/roving/RovingTabIndexWrapper.tsx b/src/accessibility/roving/RovingTabIndexWrapper.tsx
index c826b74497..5211f30215 100644
--- a/src/accessibility/roving/RovingTabIndexWrapper.tsx
+++ b/src/accessibility/roving/RovingTabIndexWrapper.tsx
@@ -16,7 +16,6 @@ limitations under the License.
 
 import React from "react";
 
-import AccessibleButton from "../../components/views/elements/AccessibleButton";
 import {useRovingTabIndex} from "../RovingTabIndex";
 import {FocusHandler, Ref} from "./types";
 
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
index 47faa35df4..0a1a0b02b3 100644
--- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
@@ -56,12 +56,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
     static propTypes = {
         hasCancel: PropTypes.bool,
         accountPassword: PropTypes.string,
-        force: PropTypes.bool,
+        forceReset: PropTypes.bool,
     };
 
     static defaultProps = {
         hasCancel: true,
-        force: false,
+        forceReset: false,
     };
 
     constructor(props) {
@@ -118,8 +118,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
                 MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
             );
 
-            const { force } = this.props;
-            const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE;
+            const { forceReset } = this.props;
+            const phase = (backupInfo && !forceReset) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE;
 
             this.setState({
                 phase,
@@ -277,20 +277,25 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
 
         const cli = MatrixClientPeg.get();
 
-        const { force } = this.props;
+        const { forceReset } = this.props;
 
         try {
-            if (force) {
-                console.log("Forcing secret storage reset"); // log something so we can debug this later
+            if (forceReset) {
+                console.log("Forcing cross-signing and secret storage reset");
                 await cli.bootstrapSecretStorage({
-                    authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
                     createSecretStorageKey: async () => this._recoveryKey,
                     setupNewKeyBackup: true,
                     setupNewSecretStorage: true,
                 });
-            } else {
-                await cli.bootstrapSecretStorage({
+                await cli.bootstrapCrossSigning({
                     authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
+                    setupNewCrossSigning: true,
+                });
+            } else {
+                await cli.bootstrapCrossSigning({
+                    authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
+                });
+                await cli.bootstrapSecretStorage({
                     createSecretStorageKey: async () => this._recoveryKey,
                     keyBackupInfo: this.state.backupInfo,
                     setupNewKeyBackup: !this.state.backupInfo,
diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx
index e7a6f44536..3ff8ff0469 100644
--- a/src/autocomplete/CommandProvider.tsx
+++ b/src/autocomplete/CommandProvider.tsx
@@ -89,7 +89,11 @@ export default class CommandProvider extends AutocompleteProvider {
 
     renderCompletions(completions: React.ReactNode[]): React.ReactNode {
         return (
-            <div className="mx_Autocomplete_Completion_container_block" role="listbox" aria-label={_t("Command Autocomplete")}>
+            <div
+                className="mx_Autocomplete_Completion_container_block"
+                role="listbox"
+                aria-label={_t("Command Autocomplete")}
+            >
                 { completions }
             </div>
         );
diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx
index f34fee890e..031fcd6169 100644
--- a/src/autocomplete/CommunityProvider.tsx
+++ b/src/autocomplete/CommunityProvider.tsx
@@ -91,15 +91,15 @@ export default class CommunityProvider extends AutocompleteProvider {
                 href: makeGroupPermalink(groupId),
                 component: (
                     <PillCompletion title={name} description={groupId}>
-                        <BaseAvatar name={name || groupId}
-                                    width={24}
-                                    height={24}
-                                    url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
+                        <BaseAvatar
+                            name={name || groupId}
+                            width={24}
+                            height={24}
+                            url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
                     </PillCompletion>
                 ),
                 range,
-            }))
-            .slice(0, 4);
+            })).slice(0, 4);
         }
         return completions;
     }
diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx
index 6ac2f4db14..4b0d35698d 100644
--- a/src/autocomplete/Components.tsx
+++ b/src/autocomplete/Components.tsx
@@ -34,9 +34,9 @@ export const TextualCompletion = forwardRef<ITextualCompletionProps, any>((props
     const {title, subtitle, description, className, ...restProps} = props;
     return (
         <div {...restProps}
-             className={classNames('mx_Autocomplete_Completion_block', className)}
-             role="option"
-             ref={ref}
+            className={classNames('mx_Autocomplete_Completion_block', className)}
+            role="option"
+            ref={ref}
         >
             <span className="mx_Autocomplete_Completion_title">{ title }</span>
             <span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
@@ -53,9 +53,9 @@ export const PillCompletion = forwardRef<IPillCompletionProps, any>((props, ref)
     const {title, subtitle, description, className, children, ...restProps} = props;
     return (
         <div {...restProps}
-             className={classNames('mx_Autocomplete_Completion_pill', className)}
-             role="option"
-             ref={ref}
+            className={classNames('mx_Autocomplete_Completion_pill', className)}
+            role="option"
+            ref={ref}
         >
             { children }
             <span className="mx_Autocomplete_Completion_title">{ title }</span>
diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx
index 147d68f5ff..eaca42b0dd 100644
--- a/src/autocomplete/EmojiProvider.tsx
+++ b/src/autocomplete/EmojiProvider.tsx
@@ -139,7 +139,11 @@ export default class EmojiProvider extends AutocompleteProvider {
 
     renderCompletions(completions: React.ReactNode[]): React.ReactNode {
         return (
-            <div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("Emoji Autocomplete")}>
+            <div
+                className="mx_Autocomplete_Completion_container_pill"
+                role="listbox"
+                aria-label={_t("Emoji Autocomplete")}
+            >
                 { completions }
             </div>
         );
diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx
index b18b2d132c..defbc8c47f 100644
--- a/src/autocomplete/RoomProvider.tsx
+++ b/src/autocomplete/RoomProvider.tsx
@@ -110,9 +110,7 @@ export default class RoomProvider extends AutocompleteProvider {
                     ),
                     range,
                 };
-            })
-            .filter((completion) => !!completion.completion && completion.completion.length > 0)
-            .slice(0, 4);
+            }).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4);
         }
         return completions;
     }
diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx
index c957b5e597..3bde4b1d07 100644
--- a/src/autocomplete/UserProvider.tsx
+++ b/src/autocomplete/UserProvider.tsx
@@ -71,8 +71,13 @@ export default class UserProvider extends AutocompleteProvider {
         }
     }
 
-    private onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean,
-                       data: IRoomTimelineData) => {
+    private onRoomTimeline = (
+        ev: MatrixEvent,
+        room: Room,
+        toStartOfTimeline: boolean,
+        removed: boolean,
+        data: IRoomTimelineData,
+    ) => {
         if (!room) return;
         if (removed) return;
         if (room.roomId !== this.room.roomId) return;
@@ -171,7 +176,11 @@ export default class UserProvider extends AutocompleteProvider {
 
     renderCompletions(completions: React.ReactNode[]): React.ReactNode {
         return (
-            <div className="mx_Autocomplete_Completion_container_pill" role="listbox" aria-label={_t("User Autocomplete")}>
+            <div
+                className="mx_Autocomplete_Completion_container_pill"
+                role="listbox"
+                aria-label={_t("User Autocomplete")}
+            >
                 { completions }
             </div>
         );
diff --git a/src/components/structures/CompatibilityPage.js b/src/components/structures/CompatibilityPage.js
deleted file mode 100644
index 1fa6068675..0000000000
--- a/src/components/structures/CompatibilityPage.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-Copyright 2019, 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.
-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 createReactClass from 'create-react-class';
-import PropTypes from 'prop-types';
-import { _t } from '../../languageHandler';
-import SdkConfig from '../../SdkConfig';
-
-export default createReactClass({
-    displayName: 'CompatibilityPage',
-    propTypes: {
-        onAccept: PropTypes.func,
-    },
-
-    getDefaultProps: function() {
-        return {
-            onAccept: function() {}, // NOP
-        };
-    },
-
-    onAccept: function() {
-        this.props.onAccept();
-    },
-
-    render: function() {
-        const brand = SdkConfig.get().brand;
-
-        return (
-        <div className="mx_CompatibilityPage">
-            <div className="mx_CompatibilityPage_box">
-                <p>{_t(
-                    "Sorry, your browser is <b>not</b> able to run %(brand)s.",
-                    {
-                        brand,
-                    },
-                    {
-                        'b': (sub) => <b>{sub}</b>,
-                    })
-                }</p>
-                <p>
-                { _t(
-                    "%(brand)s uses many advanced browser features, some of which are not available " +
-                    "or experimental in your current browser.",
-                    { brand },
-                ) }
-                </p>
-                <p>
-                { _t(
-                    'Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, ' +
-                    'or <safariLink>Safari</safariLink> for the best experience.',
-                    {},
-                    {
-                        'chromeLink': (sub) => <a href="https://www.google.com/chrome">{sub}</a>,
-                        'firefoxLink': (sub) => <a href="https://firefox.com">{sub}</a>,
-                        'safariLink': (sub) => <a href="https://apple.com/safari">{sub}</a>,
-                    },
-                )}
-                </p>
-                <p>
-                { _t(
-                    "With your current browser, the look and feel of the application may be " +
-                    "completely incorrect, and some or all features may not function. " +
-                    "If you want to try it anyway you can continue, but you are on your own in terms " +
-                    "of any issues you may encounter!",
-                ) }
-                </p>
-                <button onClick={this.onAccept}>
-                    { _t("I understand the risks and wish to continue") }
-                </button>
-            </div>
-        </div>
-        );
-    },
-});
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index 587ae2cb6b..64e0160d83 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -233,8 +233,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
         switch (ev.key) {
             case Key.TAB:
             case Key.ESCAPE:
-            // close on left and right arrows too for when it is a context menu on a <Toolbar />
-            case Key.ARROW_LEFT:
+            case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a <Toolbar />
             case Key.ARROW_RIGHT:
                 this.props.onFinished();
                 break;
diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx
index 899dfe222d..1c2295384c 100644
--- a/src/components/structures/LeftPanel.tsx
+++ b/src/components/structures/LeftPanel.tsx
@@ -377,7 +377,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
     public render(): React.ReactNode {
         const tagPanel = !this.state.showTagPanel ? null : (
             <div className="mx_LeftPanel_tagPanelContainer">
-                <TagPanel/>
+                <TagPanel />
                 {SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
             </div>
         );
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index d7f2c73a0b..e427eb92cb 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -43,11 +43,11 @@ import PlatformPeg from "../../PlatformPeg";
 import { DefaultTagID } from "../../stores/room-list/models";
 import {
     showToast as showSetPasswordToast,
-    hideToast as hideSetPasswordToast
+    hideToast as hideSetPasswordToast,
 } from "../../toasts/SetPasswordToast";
 import {
     showToast as showServerLimitToast,
-    hideToast as hideServerLimitToast
+    hideToast as hideServerLimitToast,
 } from "../../toasts/ServerLimitToast";
 import { Action } from "../../dispatcher/actions";
 import LeftPanel from "./LeftPanel";
@@ -79,6 +79,7 @@ interface IProps {
     initialEventPixelOffset: number;
     leftDisabled: boolean;
     rightDisabled: boolean;
+    // eslint-disable-next-line camelcase
     page_type: string;
     autoJoin: boolean;
     thirdPartyInvite?: object;
@@ -98,7 +99,9 @@ interface IProps {
 }
 
 interface IUsageLimit {
+    // eslint-disable-next-line camelcase
     limit_type: "monthly_active_user" | string;
+    // eslint-disable-next-line camelcase
     admin_contact?: string;
 }
 
@@ -316,10 +319,10 @@ class LoggedInView extends React.Component<IProps, IState> {
         }
     };
 
-    _calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
-        const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
+    _calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
+        const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
         if (error) {
-            usageLimitEventContent = syncErrorData.error.data;
+            usageLimitEventContent = syncError.error.data;
         }
 
         if (usageLimitEventContent) {
@@ -620,18 +623,18 @@ class LoggedInView extends React.Component<IProps, IState> {
         switch (this.props.page_type) {
             case PageTypes.RoomView:
                 pageElement = <RoomView
-                        ref={this._roomView}
-                        autoJoin={this.props.autoJoin}
-                        onRegistered={this.props.onRegistered}
-                        thirdPartyInvite={this.props.thirdPartyInvite}
-                        oobData={this.props.roomOobData}
-                        viaServers={this.props.viaServers}
-                        eventPixelOffset={this.props.initialEventPixelOffset}
-                        key={this.props.currentRoomId || 'roomview'}
-                        disabled={this.props.middleDisabled}
-                        ConferenceHandler={this.props.ConferenceHandler}
-                        resizeNotifier={this.props.resizeNotifier}
-                    />;
+                    ref={this._roomView}
+                    autoJoin={this.props.autoJoin}
+                    onRegistered={this.props.onRegistered}
+                    thirdPartyInvite={this.props.thirdPartyInvite}
+                    oobData={this.props.roomOobData}
+                    viaServers={this.props.viaServers}
+                    eventPixelOffset={this.props.initialEventPixelOffset}
+                    key={this.props.currentRoomId || 'roomview'}
+                    disabled={this.props.middleDisabled}
+                    ConferenceHandler={this.props.ConferenceHandler}
+                    resizeNotifier={this.props.resizeNotifier}
+                />;
                 break;
 
             case PageTypes.MyGroups:
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index a10af429b9..176aaf95a3 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -69,7 +69,7 @@ import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload";
 import { Action } from "../../dispatcher/actions";
 import {
     showToast as showAnalyticsToast,
-    hideToast as hideAnalyticsToast
+    hideToast as hideAnalyticsToast,
 } from "../../toasts/AnalyticsToast";
 import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
 import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
@@ -77,6 +77,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog";
 import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
 import { SettingLevel } from "../../settings/SettingLevel";
 import { leaveRoomBehaviour } from "../../utils/membership";
+import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
 
 /** constants for MatrixChat.state.view */
 export enum Views {
@@ -128,6 +129,7 @@ interface IScreen {
     params?: object;
 }
 
+/* eslint-disable camelcase */
 interface IRoomInfo {
     room_id?: string;
     room_alias?: string;
@@ -139,6 +141,7 @@ interface IRoomInfo {
     oob_data?: object;
     via_servers?: string[];
 }
+/* eslint-enable camelcase */
 
 interface IProps { // TODO type things better
     config: Record<string, any>;
@@ -164,6 +167,7 @@ interface IState {
     // the master view we are showing.
     view: Views;
     // What the LoggedInView would be showing if visible
+    // eslint-disable-next-line camelcase
     page_type?: PageTypes;
     // The ID of the room we're viewing. This is either populated directly
     // in the case where we view a room by ID or by RoomView when it resolves
@@ -179,8 +183,11 @@ interface IState {
     middleDisabled: boolean;
     // the right panel's disabled state is tracked in its store.
     // Parameters used in the registration dance with the IS
+    // eslint-disable-next-line camelcase
     register_client_secret?: string;
+    // eslint-disable-next-line camelcase
     register_session_id?: string;
+    // eslint-disable-next-line camelcase
     register_id_sid?: string;
     // When showing Modal dialogs we need to set aria-hidden on the root app element
     // and disable it when there are no dialogs
@@ -340,6 +347,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
     }
 
     // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
+    // eslint-disable-next-line camelcase
     UNSAFE_componentWillUpdate(props, state) {
         if (this.shouldTrackPageChange(this.state, state)) {
             this.startPageChangeTimer();
@@ -609,8 +617,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                 const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
                 Modal.createTrackedDialog('User settings', '', UserSettingsDialog,
                     {initialTabId: tabPayload.initialTabId},
-                    /*className=*/null, /*isPriority=*/false, /*isStatic=*/true
-                );
+                    /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
 
                 // View the welcome or home page if we need something to look at
                 this.viewSomethingBehindModal();
@@ -620,7 +627,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                 this.createRoom(payload.public);
                 break;
             case 'view_create_group': {
-                const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
+                let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
+                if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
+                    CreateGroupDialog = CreateCommunityPrototypeDialog;
+                }
                 Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
                 break;
             }
@@ -1076,7 +1086,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
             title: _t("Leave room"),
             description: (
                 <span>
-                { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
+                    { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
                     { warnings }
                 </span>
             ),
@@ -1429,7 +1439,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
         cli.on("crypto.warning", (type) => {
             switch (type) {
                 case 'CRYPTO_WARNING_OLD_VERSION_DETECTED':
-                    const brand = SdkConfig.get().brand;
                     Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, {
                         title: _t('Old cryptography data detected'),
                         description: _t(
@@ -1440,7 +1449,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
                             "in this version. This may also cause messages exchanged with this " +
                             "version to fail. If you experience problems, log out and back in " +
                             "again. To retain message history, export and re-import your keys.",
-                            { brand },
+                            { brand: SdkConfig.get().brand },
                         ),
                     });
                     break;
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index f6b8d42c30..768bc38d23 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -20,7 +20,6 @@ import classNames from "classnames";
 import defaultDispatcher from "../../dispatcher/dispatcher";
 import { _t } from "../../languageHandler";
 import { ActionPayload } from "../../dispatcher/payloads";
-import { throttle } from 'lodash';
 import { Key } from "../../Keyboard";
 import AccessibleButton from "../views/elements/AccessibleButton";
 import { Action } from "../../dispatcher/actions";
@@ -137,7 +136,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
         });
 
         let icon = (
-            <div className='mx_RoomSearch_icon'/>
+            <div className='mx_RoomSearch_icon' />
         );
         let input = (
             <input
diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx
index 704dbf8832..6bc35eb2a4 100644
--- a/src/components/structures/TabbedView.tsx
+++ b/src/components/structures/TabbedView.tsx
@@ -18,7 +18,6 @@ limitations under the License.
 
 import * as React from "react";
 import {_t} from '../../languageHandler';
-import * as PropTypes from "prop-types";
 import * as sdk from "../../index";
 import AutoHideScrollbar from './AutoHideScrollbar';
 import { ReactNode } from "react";
diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js
index 3acec417f2..a714b126ec 100644
--- a/src/components/structures/TagPanel.js
+++ b/src/components/structures/TagPanel.js
@@ -95,11 +95,6 @@ const TagPanel = createReactClass({
         }
     },
 
-    onCreateGroupClick(ev) {
-        ev.stopPropagation();
-        dis.dispatch({action: 'view_create_group'});
-    },
-
     onClearFilterClick(ev) {
         dis.dispatch({action: 'deselect_tags'});
     },
@@ -117,9 +112,7 @@ const TagPanel = createReactClass({
 
     render() {
         const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
-        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
         const ActionButton = sdk.getComponent('elements.ActionButton');
-        const TintableSvg = sdk.getComponent('elements.TintableSvg');
 
         const tags = this.state.orderedTags.map((tag, index) => {
             return <DNDTagTile
@@ -131,25 +124,29 @@ const TagPanel = createReactClass({
         });
 
         const itemsSelected = this.state.selectedTags.length > 0;
-
-        let clearButton;
-        if (itemsSelected) {
-            clearButton = <AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
-                <TintableSvg src={require("../../../res/img/icons-close.svg")} width="24" height="24"
-                             alt={_t("Clear filter")}
-                             title={_t("Clear filter")}
-                />
-            </AccessibleButton>;
-        }
-
         const classes = classNames('mx_TagPanel', {
             mx_TagPanel_items_selected: itemsSelected,
         });
 
-        return <div className={classes}>
-            <div className="mx_TagPanel_clearButton_container">
-                { clearButton }
-            </div>
+        let createButton = (
+            <ActionButton
+                tooltip
+                label={_t("Communities")}
+                action="toggle_my_groups"
+                className="mx_TagTile mx_TagTile_plus" />
+        );
+
+        if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
+            createButton = (
+                <ActionButton
+                    tooltip
+                    label={_t("Create community")}
+                    action="view_create_group"
+                    className="mx_TagTile mx_TagTile_plus" />
+            );
+        }
+
+        return <div className={classes} onClick={this.onClearFilterClick}>
             <AutoHideScrollbar
                 className="mx_TagPanel_scroller"
                 // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
@@ -168,11 +165,7 @@ const TagPanel = createReactClass({
                                 { this.renderGlobalIcon() }
                                 { tags }
                                 <div>
-                                    <ActionButton
-                                        tooltip
-                                        label={_t("Communities")}
-                                        action="toggle_my_groups"
-                                        className="mx_TagTile mx_TagTile_plus" />
+                                    {createButton}
                                 </div>
                                 { provided.placeholder }
                             </div>
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 839d4bccda..69b9c3f26e 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -40,11 +40,8 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
 import { SettingLevel } from "../../settings/SettingLevel";
 import IconizedContextMenu, {
     IconizedContextMenuOption,
-    IconizedContextMenuOptionList
+    IconizedContextMenuOptionList,
 } from "../views/context_menus/IconizedContextMenu";
-import TagOrderStore from "../../stores/TagOrderStore";
-import * as fbEmitter from "fbemitter";
-import FlairStore from "../../stores/FlairStore";
 
 interface IProps {
     isMinimized: boolean;
@@ -55,16 +52,11 @@ type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
 interface IState {
     contextMenuPosition: PartialDOMRect;
     isDarkTheme: boolean;
-    selectedCommunityProfile: {
-        displayName: string;
-        avatarMxc: string;
-    };
 }
 
 export default class UserMenu extends React.Component<IProps, IState> {
     private dispatcherRef: string;
     private themeWatcherRef: string;
-    private tagStoreRef: fbEmitter.EventSubscription;
     private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
 
     constructor(props: IProps) {
@@ -73,7 +65,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
         this.state = {
             contextMenuPosition: null,
             isDarkTheme: this.isUserOnDarkTheme(),
-            selectedCommunityProfile: null,
         };
 
         OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
@@ -86,7 +77,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
     public componentDidMount() {
         this.dispatcherRef = defaultDispatcher.register(this.onAction);
         this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
-        this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate);
     }
 
     public componentWillUnmount() {
@@ -103,25 +93,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
         return theme === "dark";
     }
 
-    private onTagStoreUpdate = async () => {
-        if (!SettingsStore.getValue("feature_communities_v2_prototypes")) {
-            return;
-        }
-
-        const selectedId = TagOrderStore.getSelectedTags()[0];
-        if (!selectedId) {
-            this.setState({selectedCommunityProfile: null});
-            return;
-        }
-
-        // For some reason the group's profile info isn't on the js-sdk Group object but
-        // is in the flair store, so get it from there.
-        const profile = await FlairStore.getGroupProfileCached(MatrixClientPeg.get(), selectedId);
-        const displayName = profile.name || selectedId;
-        const avatarMxc = profile.avatarUrl;
-        this.setState({selectedCommunityProfile: {displayName, avatarMxc}});
-    };
-
     private onProfileUpdate = async () => {
         // the store triggered an update, so force a layout update. We don't
         // have any state to store here for that to magically happen.
@@ -263,12 +234,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
         >
             <div className="mx_UserMenu_contextMenu_header">
                 <div className="mx_UserMenu_contextMenu_name">
-                        <span className="mx_UserMenu_contextMenu_displayName">
-                            {OwnProfileStore.instance.displayName}
-                        </span>
+                    <span className="mx_UserMenu_contextMenu_displayName">
+                        {OwnProfileStore.instance.displayName}
+                    </span>
                     <span className="mx_UserMenu_contextMenu_userId">
-                            {MatrixClientPeg.get().getUserId()}
-                        </span>
+                        {MatrixClientPeg.get().getUserId()}
+                    </span>
                 </div>
                 <AccessibleTooltipButton
                     className="mx_UserMenu_contextMenu_themeButton"
@@ -324,18 +295,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
     public render() {
         const avatarSize = 32; // should match border-radius of the avatar
 
-        let displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId();
-        let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
-
-        if (this.state.selectedCommunityProfile) {
-            displayName = this.state.selectedCommunityProfile.displayName
-            const mxc = this.state.selectedCommunityProfile.avatarMxc;
-            if (mxc) {
-                avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(mxc, avatarSize, avatarSize);
-            } else {
-                avatarUrl = null;
-            }
-        }
+        const displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId();
+        const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
 
         let name = <span className="mx_UserMenu_userName">{displayName}</span>;
         let buttons = (
diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx
index 7860857c55..245c50576a 100644
--- a/src/components/views/avatars/BaseAvatar.tsx
+++ b/src/components/views/avatars/BaseAvatar.tsx
@@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
+import React, {useCallback, useContext, useEffect, useState} from 'react';
 import classNames from 'classnames';
 import * as AvatarLogic from '../../../Avatar';
 import SettingsStore from "../../../settings/SettingsStore";
@@ -96,7 +96,7 @@ const BaseAvatar = (props: IProps) => {
         urls,
         width = 40,
         height = 40,
-        resizeMethod = "crop", // eslint-disable-line no-unused-vars
+        resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars
         defaultToInitialLetter = true,
         onClick,
         inputRef,
diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx
index e6dadf676c..d7e012467b 100644
--- a/src/components/views/avatars/DecoratedRoomAvatar.tsx
+++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx
@@ -126,7 +126,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
     private onPresenceUpdate = () => {
         if (this.isUnmounted) return;
 
-        let newIcon = this.getPresenceIcon();
+        const newIcon = this.getPresenceIcon();
         if (newIcon !== this.state.icon) this.setState({icon: newIcon});
     };
 
diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx
index e55e2e6fac..51327605c0 100644
--- a/src/components/views/avatars/GroupAvatar.tsx
+++ b/src/components/views/avatars/GroupAvatar.tsx
@@ -47,7 +47,7 @@ export default class GroupAvatar extends React.Component<IProps> {
     render() {
         // extract the props we use from props so we can pass any others through
         // should consider adding this as a global rule in js-sdk?
-        /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
+        /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
         const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props;
 
         return (
diff --git a/src/components/views/avatars/PulsedAvatar.tsx b/src/components/views/avatars/PulsedAvatar.tsx
index 94a6c87687..b4e876b9f6 100644
--- a/src/components/views/avatars/PulsedAvatar.tsx
+++ b/src/components/views/avatars/PulsedAvatar.tsx
@@ -25,4 +25,4 @@ const PulsedAvatar: React.FC<IProps> = (props) => {
     </div>;
 };
 
-export default PulsedAvatar;
\ No newline at end of file
+export default PulsedAvatar;
diff --git a/src/components/views/create_room/Presets.js b/src/components/views/create_room/Presets.js
deleted file mode 100644
index 0f18d11511..0000000000
--- a/src/components/views/create_room/Presets.js
+++ /dev/null
@@ -1,57 +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 { _t } from '../../../languageHandler';
-
-const Presets = {
-    PrivateChat: "private_chat",
-    PublicChat: "public_chat",
-    Custom: "custom",
-};
-
-export default createReactClass({
-    displayName: 'CreateRoomPresets',
-    propTypes: {
-        onChange: PropTypes.func,
-        preset: PropTypes.string,
-    },
-
-    Presets: Presets,
-
-    getDefaultProps: function() {
-        return {
-            onChange: function() {},
-        };
-    },
-
-    onValueChanged: function(ev) {
-        this.props.onChange(ev.target.value);
-    },
-
-    render: function() {
-        return (
-            <select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
-                <option value={this.Presets.PrivateChat}>{ _t("Private Chat") }</option>
-                <option value={this.Presets.PublicChat}>{ _t("Public Chat") }</option>
-                <option value={this.Presets.Custom}>{ _t("Custom") }</option>
-            </select>
-        );
-    },
-});
diff --git a/src/components/views/create_room/RoomAlias.js b/src/components/views/create_room/RoomAlias.js
deleted file mode 100644
index 5bdfdde08d..0000000000
--- a/src/components/views/create_room/RoomAlias.js
+++ /dev/null
@@ -1,106 +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 { _t } from '../../../languageHandler';
-
-export default createReactClass({
-    displayName: 'RoomAlias',
-    propTypes: {
-        // Specifying a homeserver will make magical things happen when you,
-        // e.g. start typing in the room alias box.
-        homeserver: PropTypes.string,
-        alias: PropTypes.string,
-        onChange: PropTypes.func,
-    },
-
-    getDefaultProps: function() {
-        return {
-            onChange: function() {},
-            alias: '',
-        };
-    },
-
-    getAliasLocalpart: function() {
-        let room_alias = this.props.alias;
-
-        if (room_alias && this.props.homeserver) {
-            const suffix = ":" + this.props.homeserver;
-            if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
-                room_alias = room_alias.slice(1, -suffix.length);
-            }
-        }
-
-        return room_alias;
-    },
-
-    onValueChanged: function(ev) {
-        this.props.onChange(ev.target.value);
-    },
-
-    onFocus: function(ev) {
-        const target = ev.target;
-        const curr_val = ev.target.value;
-
-        if (this.props.homeserver) {
-            if (curr_val == "") {
-                const self = this;
-                setTimeout(function() {
-                    target.value = "#:" + self.props.homeserver;
-                    target.setSelectionRange(1, 1);
-                }, 0);
-            } else {
-                const suffix = ":" + this.props.homeserver;
-                setTimeout(function() {
-                    target.setSelectionRange(
-                        curr_val.startsWith("#") ? 1 : 0,
-                        curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length,
-                    );
-                }, 0);
-            }
-        }
-    },
-
-    onBlur: function(ev) {
-        const curr_val = ev.target.value;
-
-        if (this.props.homeserver) {
-            if (curr_val == "#:" + this.props.homeserver) {
-                ev.target.value = "";
-                return;
-            }
-
-            if (curr_val != "") {
-                let new_val = ev.target.value;
-                const suffix = ":" + this.props.homeserver;
-                if (!curr_val.startsWith("#")) new_val = "#" + new_val;
-                if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
-                ev.target.value = new_val;
-            }
-        }
-    },
-
-    render: function() {
-        return (
-            <input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
-                onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
-                value={this.props.alias} />
-        );
-    },
-});
diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
new file mode 100644
index 0000000000..1c8a4ad6f6
--- /dev/null
+++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx
@@ -0,0 +1,248 @@
+/*
+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.
+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, { ChangeEvent, FormEvent } from 'react';
+import BaseDialog from "./BaseDialog";
+import { _t } from "../../../languageHandler";
+import { IDialogProps } from "./IDialogProps";
+import Field from "../elements/Field";
+import AccessibleButton from "../elements/AccessibleButton";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { arrayFastClone } from "../../../utils/arrays";
+import SdkConfig from "../../../SdkConfig";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import InviteDialog from "./InviteDialog";
+import BaseAvatar from "../avatars/BaseAvatar";
+import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
+import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
+import StyledCheckbox from "../elements/StyledCheckbox";
+import Modal from "../../../Modal";
+import ErrorDialog from "./ErrorDialog";
+
+interface IProps extends IDialogProps {
+    roomId: string;
+    communityName: string;
+}
+
+interface IPerson {
+    userId: string;
+    user: RoomMember;
+    lastActive: number;
+}
+
+interface IState {
+    emailTargets: string[];
+    userTargets: string[];
+    showPeople: boolean;
+    people: IPerson[];
+    numPeople: number;
+    busy: boolean;
+}
+
+export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
+    constructor(props: IProps) {
+        super(props);
+
+        this.state = {
+            emailTargets: [],
+            userTargets: [],
+            showPeople: false,
+            people: this.buildSuggestions(),
+            numPeople: 5, // arbitrary default
+            busy: false,
+        };
+    }
+
+    private buildSuggestions(): IPerson[] {
+        const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
+        if (this.props.roomId) {
+            const room = MatrixClientPeg.get().getRoom(this.props.roomId);
+            if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
+            room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
+            room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
+            // add banned users, so we don't try to invite them
+            room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
+        }
+
+        return InviteDialog.buildRecents(alreadyInvited);
+    }
+
+    private onSubmit = async (ev: FormEvent) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+
+        this.setState({busy: true});
+        try {
+            const targets = [...this.state.emailTargets, ...this.state.userTargets];
+            const result = await inviteMultipleToRoom(this.props.roomId, targets);
+            const room = MatrixClientPeg.get().getRoom(this.props.roomId);
+            const success = showAnyInviteErrors(result.states, room, result.inviter);
+            if (success) {
+                this.props.onFinished(true);
+            } else {
+                this.setState({busy: false});
+            }
+        } catch (e) {
+            this.setState({busy: false});
+            console.error(e);
+            Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
+                title: _t("Failed to invite"),
+                description: ((e && e.message) ? e.message : _t("Operation failed")),
+            });
+        }
+    };
+
+    private onAddressChange = (ev: ChangeEvent<HTMLInputElement>, index: number) => {
+        const targets = arrayFastClone(this.state.emailTargets);
+        if (index >= targets.length) {
+            targets.push(ev.target.value);
+        } else {
+            targets[index] = ev.target.value;
+        }
+        this.setState({emailTargets: targets});
+    };
+
+    private onAddressBlur = (index: number) => {
+        const targets = arrayFastClone(this.state.emailTargets);
+        if (index >= targets.length) return; // not important
+        if (targets[index].trim() === "") {
+            targets.splice(index, 1);
+            this.setState({emailTargets: targets});
+        }
+    };
+
+    private onShowPeopleClick = () => {
+        this.setState({showPeople: !this.state.showPeople});
+    };
+
+    private setPersonToggle = (person: IPerson, selected: boolean) => {
+        const targets = arrayFastClone(this.state.userTargets);
+        if (selected && !targets.includes(person.userId)) {
+            targets.push(person.userId);
+        } else if (!selected && targets.includes(person.userId)) {
+            targets.splice(targets.indexOf(person.userId), 1);
+        }
+        this.setState({userTargets: targets});
+    };
+
+    private renderPerson(person: IPerson, key: any) {
+        const avatarSize = 36;
+        return (
+            <div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
+                <BaseAvatar
+                    url={getHttpUriForMxc(
+                        MatrixClientPeg.get().getHomeserverUrl(), person.user.getMxcAvatarUrl(),
+                        avatarSize, avatarSize, "crop")}
+                    name={person.user.name}
+                    idName={person.user.userId}
+                    width={avatarSize}
+                    height={avatarSize}
+                />
+                <div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
+                    <span className="mx_CommunityPrototypeInviteDialog_personName">{person.user.name}</span>
+                    <span className="mx_CommunityPrototypeInviteDialog_personId">{person.userId}</span>
+                </div>
+                <StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
+            </div>
+        );
+    }
+
+    private onShowMorePeople = () => {
+        this.setState({numPeople: this.state.numPeople + 5}); // arbitrary increase
+    };
+
+    public render() {
+        const emailAddresses = [];
+        this.state.emailTargets.forEach((address, i) => {
+            emailAddresses.push((
+                <Field
+                    key={i}
+                    value={address}
+                    onChange={(e) => this.onAddressChange(e, i)}
+                    label={_t("Email address")}
+                    placeholder={_t("Email address")}
+                    onBlur={() => this.onAddressBlur(i)}
+                />
+            ));
+        });
+
+        // Push a clean input
+        emailAddresses.push((
+            <Field
+                key={emailAddresses.length}
+                value={""}
+                onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
+                label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
+                placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
+            />
+        ));
+
+        let peopleIntro = null;
+        const people = [];
+        if (this.state.showPeople) {
+            const humansToPresent = this.state.people.slice(0, this.state.numPeople);
+            humansToPresent.forEach((person, i) => {
+                people.push(this.renderPerson(person, i));
+            });
+            if (humansToPresent.length < this.state.people.length) {
+                people.push((
+                    <AccessibleButton
+                        onClick={this.onShowMorePeople}
+                        kind="link" key="more"
+                        className="mx_CommunityPrototypeInviteDialog_morePeople"
+                    >{_t("Show more")}</AccessibleButton>
+                ));
+            }
+        }
+        if (this.state.people.length > 0) {
+            peopleIntro = (
+                <div className="mx_CommunityPrototypeInviteDialog_people">
+                    <span>{_t("People you know on %(brand)s", {brand: SdkConfig.get().brand})}</span>
+                    <AccessibleButton onClick={this.onShowPeopleClick}>
+                        {this.state.showPeople ? _t("Hide") : _t("Show")}
+                    </AccessibleButton>
+                </div>
+            );
+        }
+
+        let buttonText = _t("Skip");
+        const targetCount = this.state.userTargets.length + this.state.emailTargets.length;
+        if (targetCount > 0) {
+            buttonText = _t("Send %(count)s invites", {count: targetCount});
+        }
+
+        return (
+            <BaseDialog
+                className="mx_CommunityPrototypeInviteDialog"
+                onFinished={this.props.onFinished}
+                title={_t("Invite people to join %(communityName)s", {communityName: this.props.communityName})}
+            >
+                <form onSubmit={this.onSubmit}>
+                    <div className="mx_Dialog_content">
+                        {emailAddresses}
+                        {peopleIntro}
+                        {people}
+                        <AccessibleButton
+                            kind="primary" onClick={this.onSubmit}
+                            disabled={this.state.busy}
+                            className="mx_CommunityPrototypeInviteDialog_primaryButton"
+                        >{buttonText}</AccessibleButton>
+                    </div>
+                </form>
+            </BaseDialog>
+        );
+    }
+}
diff --git a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
new file mode 100644
index 0000000000..1d9d92b9c9
--- /dev/null
+++ b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx
@@ -0,0 +1,227 @@
+/*
+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.
+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, { ChangeEvent } from 'react';
+import BaseDialog from "./BaseDialog";
+import { _t } from "../../../languageHandler";
+import { IDialogProps } from "./IDialogProps";
+import Field from "../elements/Field";
+import AccessibleButton from "../elements/AccessibleButton";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import InfoTooltip from "../elements/InfoTooltip";
+import dis from "../../../dispatcher/dispatcher";
+import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
+import GroupStore from "../../../stores/GroupStore";
+
+interface IProps extends IDialogProps {
+}
+
+interface IState {
+    name: string;
+    localpart: string;
+    error: string;
+    busy: boolean;
+    avatarFile: File;
+    avatarPreview: string;
+}
+
+export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
+    private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
+
+    constructor(props: IProps) {
+        super(props);
+
+        this.state = {
+            name: "",
+            localpart: "",
+            error: null,
+            busy: false,
+            avatarFile: null,
+            avatarPreview: null,
+        };
+    }
+
+    private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
+        const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-');
+        this.setState({name: ev.target.value, localpart});
+    };
+
+    private onSubmit = async (ev) => {
+        ev.preventDefault();
+        ev.stopPropagation();
+
+        if (this.state.busy) return;
+
+        // We'll create the community now to see if it's taken, leaving it active in
+        // the background for the user to look at while they invite people.
+        this.setState({busy: true});
+        try {
+            let avatarUrl = ''; // must be a string for synapse to accept it
+            if (this.state.avatarFile) {
+                avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
+            }
+
+            const result = await MatrixClientPeg.get().createGroup({
+                localpart: this.state.localpart,
+                profile: {
+                    name: this.state.name,
+                    avatar_url: avatarUrl,
+                },
+            });
+
+            // Ensure the tag gets selected now that we've created it
+            dis.dispatch({action: 'deselect_tags'}, true);
+            dis.dispatch({
+                action: 'select_tag',
+                tag: result.group_id,
+            });
+
+            // Close our own dialog before moving much further
+            this.props.onFinished(true);
+
+            if (result.room_id) {
+                // Force the group store to update as it might have missed the general chat
+                await GroupStore.refreshGroupRooms(result.group_id);
+                dis.dispatch({
+                    action: 'view_room',
+                    room_id: result.room_id,
+                });
+                showCommunityRoomInviteDialog(result.room_id, this.state.name);
+            } else {
+                dis.dispatch({
+                    action: 'view_group',
+                    group_id: result.group_id,
+                    group_is_new: true,
+                });
+            }
+        } catch (e) {
+            console.error(e);
+            this.setState({
+                busy: false,
+                error: _t(
+                    "There was an error creating your community. The name may be taken or the " +
+                    "server is unable to process your request.",
+                ),
+            });
+        }
+    };
+
+    private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
+        if (!e.target.files || !e.target.files.length) {
+            this.setState({avatarFile: null});
+        } else {
+            this.setState({busy: true});
+            const file = e.target.files[0];
+            const reader = new FileReader();
+            reader.onload = (ev: ProgressEvent<FileReader>) => {
+                this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string});
+            };
+            reader.readAsDataURL(file);
+        }
+    };
+
+    private onChangeAvatar = () => {
+        if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
+    };
+
+    public render() {
+        let communityId = null;
+        if (this.state.localpart) {
+            communityId = (
+                <span className="mx_CreateCommunityPrototypeDialog_communityId">
+                    {_t("Community ID: +<localpart />:%(domain)s", {
+                        domain: MatrixClientPeg.getHomeserverName(),
+                    }, {
+                        localpart: () => <u>{this.state.localpart}</u>,
+                    })}
+                    <InfoTooltip
+                        tooltip={_t(
+                            "Use this when referencing your community to others. The community ID " +
+                            "cannot be changed.",
+                        )}
+                    />
+                </span>
+            );
+        }
+
+        let helpText = (
+            <span className="mx_CreateCommunityPrototypeDialog_subtext">
+                {_t("You can change this later if needed.")}
+            </span>
+        );
+        if (this.state.error) {
+            const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error";
+            helpText = (
+                <span className={classes}>
+                    {this.state.error}
+                </span>
+            );
+        }
+
+        let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
+        if (!this.state.avatarPreview) {
+            preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />
+        }
+
+        return (
+            <BaseDialog
+                className="mx_CreateCommunityPrototypeDialog"
+                onFinished={this.props.onFinished}
+                title={_t("What's the name of your community or team?")}
+            >
+                <form onSubmit={this.onSubmit}>
+                    <div className="mx_Dialog_content">
+                        <div className="mx_CreateCommunityPrototypeDialog_colName">
+                            <Field
+                                value={this.state.name}
+                                onChange={this.onNameChange}
+                                placeholder={_t("Enter name")}
+                                label={_t("Enter name")}
+                            />
+                            {helpText}
+                            <span className="mx_CreateCommunityPrototypeDialog_subtext">
+                                {/*nbsp is to reserve the height of this element when there's nothing*/}
+                                &nbsp;{communityId}
+                            </span>
+                            <AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
+                                {_t("Create")}
+                            </AccessibleButton>
+                        </div>
+                        <div className="mx_CreateCommunityPrototypeDialog_colAvatar">
+                            <input
+                                type="file" style={{display: "none"}}
+                                ref={this.avatarUploadRef} accept="image/*"
+                                onChange={this.onAvatarChanged}
+                            />
+                            <AccessibleButton
+                                onClick={this.onChangeAvatar}
+                                className="mx_CreateCommunityPrototypeDialog_avatarContainer"
+                            >
+                                {preview}
+                            </AccessibleButton>
+                            <div className="mx_CreateCommunityPrototypeDialog_tip">
+                                <b>{_t("Add image (optional)")}</b>
+                                <span>
+                                    {_t("An image will help people identify your community.")}
+                                </span>
+                            </div>
+                        </div>
+                    </div>
+                </form>
+            </BaseDialog>
+        );
+    }
+}
diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js
index d8a8b96961..10285ccee0 100644
--- a/src/components/views/dialogs/CreateGroupDialog.js
+++ b/src/components/views/dialogs/CreateGroupDialog.js
@@ -83,18 +83,11 @@ export default createReactClass({
             localpart: this.state.groupId,
             profile: profile,
         }).then((result) => {
-            if (result.room_id) {
-                dis.dispatch({
-                    action: 'view_room',
-                    room_id: result.room_id,
-                });
-            } else {
-                dis.dispatch({
-                    action: 'view_group',
-                    group_id: result.group_id,
-                    group_is_new: true,
-                });
-            }
+            dis.dispatch({
+                action: 'view_group',
+                group_id: result.group_id,
+                group_is_new: true,
+            });
             this.props.onFinished(true);
         }).catch((e) => {
             this.setState({createError: e});
diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js
index ce7ac6e59c..4890626527 100644
--- a/src/components/views/dialogs/CreateRoomDialog.js
+++ b/src/components/views/dialogs/CreateRoomDialog.js
@@ -25,6 +25,8 @@ import { _t } from '../../../languageHandler';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
 import {Key} from "../../../Keyboard";
 import {privateShouldBeEncrypted} from "../../../createRoom";
+import TagOrderStore from "../../../stores/TagOrderStore";
+import GroupStore from "../../../stores/GroupStore";
 
 export default createReactClass({
     displayName: 'CreateRoomDialog',
@@ -70,6 +72,10 @@ export default createReactClass({
             opts.encryption = this.state.isEncrypted;
         }
 
+        if (TagOrderStore.getSelectedPrototypeTag()) {
+            opts.associatedWithCommunity = TagOrderStore.getSelectedPrototypeTag();
+        }
+
         return opts;
     },
 
@@ -178,18 +184,25 @@ export default createReactClass({
         const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
         const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
 
-        let publicPrivateLabel;
         let aliasField;
         if (this.state.isPublic) {
-            publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
             const domain = MatrixClientPeg.get().getDomain();
             aliasField = (
                 <div className="mx_CreateRoomDialog_aliasContainer">
                     <RoomAliasField ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
                 </div>
             );
-        } else {
-            publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
+        }
+
+        let publicPrivateLabel = <p>{_t(
+            "Private rooms can be found and joined by invitation only. Public rooms can be " +
+            "found and joined by anyone.",
+        )}</p>;
+        if (TagOrderStore.getSelectedPrototypeTag()) {
+            publicPrivateLabel = <p>{_t(
+                "Private rooms can be found and joined by invitation only. Public rooms can be " +
+                "found and joined by anyone in this community.",
+            )}</p>;
         }
 
         let e2eeSection;
@@ -212,7 +225,25 @@ export default createReactClass({
             </React.Fragment>;
         }
 
-        const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
+        let federateLabel = _t(
+            "You might enable this if the room will only be used for collaborating with internal " +
+            "teams on your homeserver. This cannot be changed later.",
+        );
+        if (SdkConfig.get().default_federate === false) {
+            // We only change the label if the default setting is different to avoid jarring text changes to the
+            // user. They will have read the implications of turning this off/on, so no need to rephrase for them.
+            federateLabel = _t(
+                "You might disable this if the room will be used for collaborating with external " +
+                "teams who have their own homeserver. This cannot be changed later.",
+            );
+        }
+
+        let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
+        if (TagOrderStore.getSelectedPrototypeTag()) {
+            const summary = GroupStore.getSummary(TagOrderStore.getSelectedPrototypeTag());
+            const name = summary?.profile?.name || TagOrderStore.getSelectedPrototypeTag();
+            title = _t("Create a room in %(communityName)s", {communityName: name});
+        }
         return (
             <BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
                 title={title}
@@ -227,7 +258,15 @@ export default createReactClass({
                         { aliasField }
                         <details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
                             <summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
-                            <LabelledToggleSwitch label={ _t('Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)')} onChange={this.onNoFederateChange} value={this.state.noFederate} />
+                            <LabelledToggleSwitch
+                                label={_t(
+                                    "Block anyone not part of %(serverName)s from ever joining this room.",
+                                    {serverName: MatrixClientPeg.getHomeserverName()},
+                                )}
+                                onChange={this.onNoFederateChange}
+                                value={this.state.noFederate}
+                            />
+                            <p>{federateLabel}</p>
                         </details>
                     </div>
                 </form>
diff --git a/src/components/views/dialogs/IDialogProps.ts b/src/components/views/dialogs/IDialogProps.ts
new file mode 100644
index 0000000000..1027ca7607
--- /dev/null
+++ b/src/components/views/dialogs/IDialogProps.ts
@@ -0,0 +1,19 @@
+/*
+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.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+export interface IDialogProps {
+    onFinished: (bool) => void;
+}
diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js
index c90811ed5a..6cd0b22505 100644
--- a/src/components/views/dialogs/InviteDialog.js
+++ b/src/components/views/dialogs/InviteDialog.js
@@ -327,7 +327,7 @@ export default class InviteDialog extends React.PureComponent {
         this.state = {
             targets: [], // array of Member objects (see interface above)
             filterText: "",
-            recents: this._buildRecents(alreadyInvited),
+            recents: InviteDialog.buildRecents(alreadyInvited),
             numRecentsShown: INITIAL_ROOMS_SHOWN,
             suggestions: this._buildSuggestions(alreadyInvited),
             numSuggestionsShown: INITIAL_ROOMS_SHOWN,
@@ -344,7 +344,7 @@ export default class InviteDialog extends React.PureComponent {
         this._editorRef = createRef();
     }
 
-    _buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
+    static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
         const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
 
         // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx
index f6767dcb8d..81f628343b 100644
--- a/src/components/views/dialogs/ServerOfflineDialog.tsx
+++ b/src/components/views/dialogs/ServerOfflineDialog.tsx
@@ -27,9 +27,9 @@ import Spinner from "../elements/Spinner";
 import AccessibleButton from "../elements/AccessibleButton";
 import { UPDATE_EVENT } from "../../../stores/AsyncStore";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { IDialogProps } from "./IDialogProps";
 
-interface IProps {
-    onFinished: (bool) => void;
+interface IProps extends IDialogProps {
 }
 
 export default class ServerOfflineDialog extends React.PureComponent<IProps> {
diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx
index dc2a987f13..e849f7efe3 100644
--- a/src/components/views/dialogs/ShareDialog.tsx
+++ b/src/components/views/dialogs/ShareDialog.tsx
@@ -31,6 +31,7 @@ import {toRightOf} from "../../structures/ContextMenu";
 import {copyPlaintext, selectText} from "../../../utils/strings";
 import StyledCheckbox from '../elements/StyledCheckbox';
 import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
+import { IDialogProps } from "./IDialogProps";
 
 const socials = [
     {
@@ -60,8 +61,7 @@ const socials = [
     },
 ];
 
-interface IProps {
-    onFinished: () => void;
+interface IProps extends IDialogProps {
     target: Room | User | Group | RoomMember | MatrixEvent;
     permalinkCreator: RoomPermalinkCreator;
 }
@@ -186,8 +186,8 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
             title = _t('Share Room Message');
             checkbox = <div>
                 <StyledCheckbox
-                       checked={this.state.linkSpecificEvent}
-                       onClick={this.onLinkSpecificEventCheckboxClick}
+                    checked={this.state.linkSpecificEvent}
+                    onClick={this.onLinkSpecificEventCheckboxClick}
                 >
                     { _t('Link to selected message') }
                 </StyledCheckbox>
@@ -198,16 +198,18 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
         const encodedUrl = encodeURIComponent(matrixToUrl);
 
         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
-        return <BaseDialog title={title}
-                           className='mx_ShareDialog'
-                           contentId='mx_Dialog_content'
-                           onFinished={this.props.onFinished}
+        return <BaseDialog
+            title={title}
+            className='mx_ShareDialog'
+            contentId='mx_Dialog_content'
+            onFinished={this.props.onFinished}
         >
             <div className="mx_ShareDialog_content">
                 <div className="mx_ShareDialog_matrixto">
-                    <a href={matrixToUrl}
-                       onClick={ShareDialog.onLinkClick}
-                       className="mx_ShareDialog_matrixto_link"
+                    <a
+                        href={matrixToUrl}
+                        onClick={ShareDialog.onLinkClick}
+                        className="mx_ShareDialog_matrixto_link"
                     >
                         { matrixToUrl }
                     </a>
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index a52dea3e0a..299025f949 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -735,7 +735,7 @@ export default class AppTile extends React.Component {
 
         // Additional iframe feature pemissions
         // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
-        const iframeFeatures = "microphone; camera; encrypted-media; autoplay;";
+        const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture;";
 
         const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini  ' : ' ');
 
diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx
index 3397fd901c..a6eb8323f3 100644
--- a/src/components/views/elements/Draggable.tsx
+++ b/src/components/views/elements/Draggable.tsx
@@ -34,7 +34,6 @@ export interface ILocationState {
 }
 
 export default class Draggable extends React.Component<IProps, IState> {
-
     constructor(props: IProps) {
         super(props);
 
@@ -77,5 +76,4 @@ export default class Draggable extends React.Component<IProps, IState> {
     render() {
         return <div className={this.props.className} onMouseDown={this.onMouseDown.bind(this)} />;
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index 7d8b774955..61e5f5381d 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -39,11 +39,13 @@ interface IProps {
     className: string;
 }
 
+/* eslint-disable camelcase */
 interface IState {
     userId: string;
     displayname: string;
     avatar_url: string;
 }
+/* eslint-enable camelcase */
 
 const AVATAR_SIZE = 32;
 
@@ -63,19 +65,18 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
         const client = MatrixClientPeg.get();
         const userId = client.getUserId();
         const profileInfo = await client.getProfileInfo(userId);
-        const avatar_url = Avatar.avatarUrlForUser(
+        const avatarUrl = Avatar.avatarUrlForUser(
             {avatarUrl: profileInfo.avatar_url},
             AVATAR_SIZE, AVATAR_SIZE, "crop");
 
         this.setState({
             userId,
             displayname: profileInfo.displayname,
-            avatar_url,
+            avatar_url: avatarUrl,
         });
-
     }
 
-    private fakeEvent({userId, displayname, avatar_url}: IState) {
+    private fakeEvent({userId, displayname, avatar_url: avatarUrl}: IState) {
         // Fake it till we make it
         const event = new MatrixEvent(JSON.parse(`{
                 "type": "m.room.message",
@@ -85,12 +86,12 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
                     "msgtype": "m.text",
                     "body": "${this.props.message}",
                     "displayname": "${displayname}",
-                    "avatar_url": "${avatar_url}"
+                    "avatar_url": "${avatarUrl}"
                   },
                   "msgtype": "m.text",
                   "body": "${this.props.message}",
                   "displayname": "${displayname}",
-                  "avatar_url": "${avatar_url}"
+                  "avatar_url": "${avatarUrl}"
                 },
                 "unsigned": {
                   "age": 97
@@ -104,7 +105,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
             name: displayname,
             userId: userId,
             getAvatarUrl: (..._) => {
-                return avatar_url;
+                return avatarUrl;
             },
         };
 
@@ -114,13 +115,10 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
     public render() {
         const event = this.fakeEvent(this.state);
 
-        let className = classnames(
-            this.props.className,
-            {
-                "mx_IRCLayout": this.props.useIRCLayout,
-                "mx_GroupLayout": !this.props.useIRCLayout,
-            }
-        );
+        const className = classnames(this.props.className, {
+            "mx_IRCLayout": this.props.useIRCLayout,
+            "mx_GroupLayout": !this.props.useIRCLayout,
+        });
 
         return <div className={className}>
             <EventTile mxEvent={event} useIRCLayout={this.props.useIRCLayout} />
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index d9fd59dc11..d2869f68c8 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -198,11 +198,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
         }
     }
 
-
-
     public render() {
-        const {
-            element, prefixComponent, postfixComponent, className, onValidate, children,
+        /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
+        const { element, prefixComponent, postfixComponent, className, onValidate, children,
             tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props;
 
         // Set some defaults for the <input> element
diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx
index 1098d0293e..ecd63816de 100644
--- a/src/components/views/elements/IRCTimelineProfileResizer.tsx
+++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx
@@ -78,7 +78,12 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I
 
     private onMoueUp(event: MouseEvent) {
         if (this.props.roomId) {
-            SettingsStore.setValue("ircDisplayNameWidth", this.props.roomId, SettingLevel.ROOM_DEVICE, this.state.width);
+            SettingsStore.setValue(
+                "ircDisplayNameWidth",
+                this.props.roomId,
+                SettingLevel.ROOM_DEVICE,
+                this.state.width,
+            );
         }
     }
 
diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx
new file mode 100644
index 0000000000..dd21c95b74
--- /dev/null
+++ b/src/components/views/elements/InfoTooltip.tsx
@@ -0,0 +1,72 @@
+/*
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+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 classNames from 'classnames';
+
+import Tooltip from './Tooltip';
+import { _t } from "../../../languageHandler";
+
+interface ITooltipProps {
+    tooltip?: React.ReactNode;
+    tooltipClassName?: string;
+}
+
+interface IState {
+    hover: boolean;
+}
+
+export default class InfoTooltip extends React.PureComponent<ITooltipProps, IState> {
+    constructor(props: ITooltipProps) {
+        super(props);
+        this.state = {
+            hover: false,
+        };
+    }
+
+    onMouseOver = () => {
+        this.setState({
+            hover: true,
+        });
+    };
+
+    onMouseLeave = () => {
+        this.setState({
+            hover: false,
+        });
+    };
+
+    render() {
+        const {tooltip, children, tooltipClassName} = this.props;
+        const title = _t("Information");
+
+        // Tooltip are forced on the right for a more natural feel to them on info icons
+        const tip = this.state.hover ? <Tooltip
+            className="mx_InfoTooltip_container"
+            tooltipClassName={classNames("mx_InfoTooltip_tooltip", tooltipClassName)}
+            label={tooltip || title}
+            forceOnRight={true}
+        /> : <div />;
+        return (
+            <div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">
+                <span className="mx_InfoTooltip_icon" aria-label={title} />
+                {children}
+                {tip}
+            </div>
+        );
+    }
+}
diff --git a/src/components/views/elements/QRCode.tsx b/src/components/views/elements/QRCode.tsx
index f70ab48fa3..9ce3dc7202 100644
--- a/src/components/views/elements/QRCode.tsx
+++ b/src/components/views/elements/QRCode.tsx
@@ -41,7 +41,7 @@ const QRCode: React.FC<IProps> = ({data, className, ...options}) => {
         return () => {
             cancelled = true;
         };
-    }, [JSON.stringify(data), options]);
+    }, [JSON.stringify(data), options]); // eslint-disable-line react-hooks/exhaustive-deps
 
     return <div className={classNames("mx_QRCode", className)}>
         { dataUri ? <img src={dataUri} className="mx_VerificationQRCode" alt={_t("QR Code")} /> : <Spinner /> }
diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx
index a88c581d07..b7c8e1b533 100644
--- a/src/components/views/elements/Slider.tsx
+++ b/src/components/views/elements/Slider.tsx
@@ -45,7 +45,7 @@ export default class Slider extends React.Component<IProps> {
     // non linear slider.
     private offset(values: number[], value: number): number {
         // the index of the first number greater than value.
-        let closest = values.reduce((prev, curr) => {
+        const closest = values.reduce((prev, curr) => {
             return (value > curr ? prev + 1 : prev);
         }, 0);
 
@@ -68,17 +68,16 @@ export default class Slider extends React.Component<IProps> {
         const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue);
 
         return 100 * (closest - 1 + linearInterpolation) * intervalWidth;
-
     }
 
     render(): React.ReactNode {
-        const dots = this.props.values.map(v =>
-            <Dot active={v <= this.props.value}
-                 label={this.props.displayFunc(v)}
-                 onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)}
-                 key={v}
-                 disabled={this.props.disabled}
-            />);
+        const dots = this.props.values.map(v => <Dot
+            active={v <= this.props.value}
+            label={this.props.displayFunc(v)}
+            onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)}
+            key={v}
+            disabled={this.props.disabled}
+        />);
 
         let selection = null;
 
@@ -93,7 +92,7 @@ export default class Slider extends React.Component<IProps> {
         return <div className="mx_Slider">
             <div>
                 <div className="mx_Slider_bar">
-                    <hr onClick={this.props.disabled ? () => {} : this.onClick.bind(this)}/>
+                    <hr onClick={this.props.disabled ? () => {} : this.onClick.bind(this)} />
                     { selection }
                 </div>
                 <div className="mx_Slider_dotContainer">
diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx
index be983828ff..f8d2665d07 100644
--- a/src/components/views/elements/StyledCheckbox.tsx
+++ b/src/components/views/elements/StyledCheckbox.tsx
@@ -17,8 +17,6 @@ limitations under the License.
 import React from "react";
 import { randomString } from "matrix-js-sdk/src/randomstring";
 
-const CHECK_BOX_SVG = require("../../../../res/img/feather-customised/check.svg");
-
 interface IProps extends React.InputHTMLAttributes<HTMLInputElement> {
 }
 
@@ -39,13 +37,14 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
     }
 
     public render() {
+        /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
         const { children, className, ...otherProps } = this.props;
         return <span className={"mx_Checkbox " + className}>
             <input id={this.id} {...otherProps} type="checkbox" />
             <label htmlFor={this.id}>
                 {/* Using the div to center the image */}
                 <div className="mx_Checkbox_background">
-                    <img src={CHECK_BOX_SVG}/>
+                    <img src={require("../../../../res/img/feather-customised/check.svg")} />
                 </div>
                 <div>
                     { this.props.children }
@@ -53,4 +52,4 @@ export default class StyledCheckbox extends React.PureComponent<IProps, IState>
             </label>
         </span>;
     }
-}
\ No newline at end of file
+}
diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js
index 49b336a577..db5eedc274 100644
--- a/src/components/views/elements/TagTile.js
+++ b/src/components/views/elements/TagTile.js
@@ -141,9 +141,12 @@ export default createReactClass({
             profile.avatarUrl, avatarHeight, avatarHeight, "crop",
         ) : null;
 
+        const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
         const className = classNames({
             mx_TagTile: true,
-            mx_TagTile_selected: this.props.selected,
+            mx_TagTile_prototype: isPrototype,
+            mx_TagTile_selected: this.props.selected && !isPrototype,
+            mx_TagTile_selected_prototype: this.props.selected && isPrototype,
         });
 
         const badge = TagOrderStore.getGroupBadge(this.props.tag);
diff --git a/src/components/views/elements/UserSelector.js b/src/components/views/elements/UserSelector.js
deleted file mode 100644
index ffb577e090..0000000000
--- a/src/components/views/elements/UserSelector.js
+++ /dev/null
@@ -1,76 +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, {createRef} from 'react';
-import PropTypes from 'prop-types';
-import createReactClass from 'create-react-class';
-import { _t } from '../../../languageHandler';
-
-export default createReactClass({
-    displayName: 'UserSelector',
-
-    propTypes: {
-        onChange: PropTypes.func,
-        selected_users: PropTypes.arrayOf(PropTypes.string),
-    },
-
-    getDefaultProps: function() {
-        return {
-            onChange: function() {},
-            selected: [],
-        };
-    },
-
-    // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
-    UNSAFE_componentWillMount: function() {
-        this._user_id_input = createRef();
-    },
-
-    addUser: function(user_id) {
-        if (this.props.selected_users.indexOf(user_id == -1)) {
-            this.props.onChange(this.props.selected_users.concat([user_id]));
-        }
-    },
-
-    removeUser: function(user_id) {
-        this.props.onChange(this.props.selected_users.filter(function(e) {
-            return e != user_id;
-        }));
-    },
-
-    onAddUserId: function() {
-        this.addUser(this._user_id_input.current.value);
-        this._user_id_input.current.value = "";
-    },
-
-    render: function() {
-        const self = this;
-        return (
-            <div>
-                <ul className="mx_UserSelector_UserIdList">
-                    { this.props.selected_users.map(function(user_id, i) {
-                        return <li key={user_id}>{ user_id } - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
-                    }) }
-                </ul>
-                <input type="text" ref={this._user_id_input} defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
-                <button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
-                    { _t("Add User") }
-                </button>
-            </div>
-        );
-    },
-});
diff --git a/src/components/views/elements/UserTagTile.tsx b/src/components/views/elements/UserTagTile.tsx
index c652423753..912f54edc7 100644
--- a/src/components/views/elements/UserTagTile.tsx
+++ b/src/components/views/elements/UserTagTile.tsx
@@ -16,16 +16,14 @@ limitations under the License.
 
 import React from "react";
 import defaultDispatcher from "../../../dispatcher/dispatcher";
-import { OwnProfileStore } from "../../../stores/OwnProfileStore";
-import { UPDATE_EVENT } from "../../../stores/AsyncStore";
 import * as fbEmitter from "fbemitter";
 import TagOrderStore from "../../../stores/TagOrderStore";
 import AccessibleTooltipButton from "./AccessibleTooltipButton";
-import BaseAvatar from "../avatars/BaseAvatar";
-import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import classNames from "classnames";
+import { _t } from "../../../languageHandler";
 
-interface IProps{}
+interface IProps {
+}
 
 interface IState {
     selected: boolean;
@@ -43,18 +41,13 @@ export default class UserTagTile extends React.PureComponent<IProps, IState> {
     }
 
     public componentDidMount() {
-        OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
         this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate);
     }
 
     public componentWillUnmount() {
-        OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
+        this.tagStoreRef.remove();
     }
 
-    private onProfileUpdate = () => {
-        this.forceUpdate();
-    };
-
     private onTagStoreUpdate = () => {
         const selected = TagOrderStore.getSelectedTags().length === 0;
         this.setState({selected});
@@ -71,27 +64,20 @@ export default class UserTagTile extends React.PureComponent<IProps, IState> {
     public render() {
         // XXX: We reuse TagTile classes for ease of demonstration - we should probably generify
         // TagTile instead if we continue to use this component.
-        const avatarHeight = 36;
-        const name = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId();
         const className = classNames({
             mx_TagTile: true,
-            mx_TagTile_selected: this.state.selected,
-            mx_TagTile_large: true,
+            mx_TagTile_prototype: true,
+            mx_TagTile_selected_prototype: this.state.selected,
+            mx_TagTile_home: true,
         });
         return (
             <AccessibleTooltipButton
                 className={className}
                 onClick={this.onTileClick}
-                title={name}
+                title={_t("Home")}
             >
                 <div className="mx_TagTile_avatar">
-                    <BaseAvatar
-                        name={name}
-                        idName={MatrixClientPeg.get().getUserId()}
-                        url={OwnProfileStore.instance.getHttpAvatarUrl(avatarHeight)}
-                        width={avatarHeight}
-                        height={avatarHeight}
-                    />
+                    <div className="mx_TagTile_homeIcon" />
                 </div>
             </AccessibleTooltipButton>
         );
diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js
index 6c00921b45..786facc340 100644
--- a/src/components/views/messages/UnknownBody.js
+++ b/src/components/views/messages/UnknownBody.js
@@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from "react";
+import React, {forwardRef} from "react";
 
-export default ({mxEvent}) => {
+export default forwardRef(({mxEvent}, ref) => {
     const text = mxEvent.getContent().body;
     return (
-        <span className="mx_UnknownBody">
+        <span className="mx_UnknownBody" ref={ref}>
             { text }
         </span>
     );
-};
+});
diff --git a/src/components/views/right_panel/EncryptionInfo.tsx b/src/components/views/right_panel/EncryptionInfo.tsx
index f62af65543..10d35200bd 100644
--- a/src/components/views/right_panel/EncryptionInfo.tsx
+++ b/src/components/views/right_panel/EncryptionInfo.tsx
@@ -76,14 +76,16 @@ const EncryptionInfo: React.FC<IProps> = ({
         description = (
             <div>
                 <p>{_t("Messages in this room are end-to-end encrypted.")}</p>
-                <p>{_t("Your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
+                <p>{_t("Your messages are secured and only you and the recipient have " +
+                    "the unique keys to unlock them.")}</p>
             </div>
         );
     } else {
         description = (
             <div>
                 <p>{_t("Messages in this room are not end-to-end encrypted.")}</p>
-                <p>{_t("In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.")}</p>
+                <p>{_t("In encrypted rooms, your messages are secured and only you and the recipient have " +
+                    "the unique keys to unlock them.")}</p>
             </div>
         );
     }
diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx
index 57d3075739..bbb783ccb9 100644
--- a/src/components/views/right_panel/HeaderButtons.tsx
+++ b/src/components/views/right_panel/HeaderButtons.tsx
@@ -23,7 +23,10 @@ import dis from '../../../dispatcher/dispatcher';
 import RightPanelStore from "../../../stores/RightPanelStore";
 import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
 import {Action} from '../../../dispatcher/actions';
-import {SetRightPanelPhasePayload, SetRightPanelPhaseRefireParams} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
+import {
+    SetRightPanelPhasePayload,
+    SetRightPanelPhaseRefireParams,
+} from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
 import {EventSubscription} from "fbemitter";
 
 export enum HeaderKind {
@@ -38,7 +41,7 @@ interface IState {
 
 interface IProps {}
 
-export default class HeaderButtons extends React.Component<IProps, IState> {
+export default abstract class HeaderButtons extends React.Component<IProps, IState> {
     private storeToken: EventSubscription;
     private dispatcherRef: string;
 
@@ -92,14 +95,7 @@ export default class HeaderButtons extends React.Component<IProps, IState> {
     }
 
     // XXX: Make renderButtons a prop
-    public renderButtons(): JSX.Element[] {
-        // Ignore - intended to be overridden by subclasses
-        // Return empty fragment to satisfy the type
-        return [
-          <React.Fragment>
-          </React.Fragment>
-        ];
-    }
+    public abstract renderButtons(): JSX.Element[];
 
     public render() {
         // inline style as this will be swapped around in future commits
diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx
index 1f0703839f..f584a63209 100644
--- a/src/components/views/right_panel/VerificationPanel.tsx
+++ b/src/components/views/right_panel/VerificationPanel.tsx
@@ -30,8 +30,6 @@ import {_t} from "../../../languageHandler";
 import SdkConfig from "../../../SdkConfig";
 import E2EIcon from "../rooms/E2EIcon";
 import {
-    PHASE_UNSENT,
-    PHASE_REQUESTED,
     PHASE_READY,
     PHASE_DONE,
     PHASE_STARTED,
@@ -104,14 +102,15 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
                     </div>;
             }
             if (showSAS) {
-                sasBlockDialog =
-                    <div className='mx_VerificationPanel_QRPhase_startOption'>
-                        <p>{_t("Compare unique emoji")}</p>
-                        <span className='mx_VerificationPanel_QRPhase_helpText'>{_t("Compare a unique set of emoji if you don't have a camera on either device")}</span>
-                        <AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this.startSAS} kind='primary'>
-                            {_t("Start")}
-                        </AccessibleButton>
-                    </div>;
+                sasBlockDialog = <div className='mx_VerificationPanel_QRPhase_startOption'>
+                    <p>{_t("Compare unique emoji")}</p>
+                    <span className='mx_VerificationPanel_QRPhase_helpText'>
+                        {_t("Compare a unique set of emoji if you don't have a camera on either device")}
+                    </span>
+                    <AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this.startSAS} kind='primary'>
+                        {_t("Start")}
+                    </AccessibleButton>
+                </div>;
             }
             const or = qrBlockDialog && sasBlockDialog ?
                 <div className='mx_VerificationPanel_QRPhase_betweenText'>{_t("or")}</div> : null;
@@ -165,8 +164,8 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
         }
 
         const noCommonMethodBlock = noCommonMethodError ?
-             <div className="mx_UserInfo_container">{noCommonMethodError}</div> :
-             null;
+            <div className="mx_UserInfo_container">{noCommonMethodError}</div> :
+            null;
 
         // TODO: add way to open camera to scan a QR code
         return <React.Fragment>
diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx
index 8253940d4e..2957b25779 100644
--- a/src/components/views/rooms/NotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge.tsx
@@ -92,6 +92,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
     };
 
     public render(): React.ReactElement {
+        /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
         const {notification, forceCount, roomId, onClick, ...props} = this.props;
 
         // Don't show a badge if we don't need to
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 3274e0e49f..ec8c8d6840 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -45,6 +45,7 @@ import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
 import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
 import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
 import AccessibleButton from "../elements/AccessibleButton";
+import TagOrderStore from "../../../stores/TagOrderStore";
 
 interface IProps {
     onKeyDown: (ev: React.KeyboardEvent) => void;
@@ -129,7 +130,9 @@ const TAG_AESTHETICS: {
                     }}
                 />
                 <IconizedContextMenuOption
-                    label={_t("Explore public rooms")}
+                    label={TagOrderStore.getSelectedPrototypeTag()
+                        ? _t("Explore community rooms")
+                        : _t("Explore public rooms")}
                     iconClassName="mx_RoomList_iconExplore"
                     onClick={(e) => {
                         e.preventDefault();
@@ -215,7 +218,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
 
     private getRoomDelta = (roomId: string, delta: number, unread = false) => {
         const lists = RoomListStore.instance.orderedLists;
-        let rooms: Room = [];
+        const rooms: Room = [];
         TAG_ORDER.forEach(t => {
             let listRooms = lists[t];
 
@@ -287,7 +290,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
         // TODO: Put community invites in a more sensible place (not in the room list)
         // See https://github.com/vector-im/element-web/issues/14456
         return MatrixClientPeg.get().getGroups().filter(g => {
-           return g.myMembership === 'invite';
+            return g.myMembership === 'invite';
         }).map(g => {
             const avatar = (
                 <GroupAvatar
@@ -343,21 +346,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
                 : TAG_AESTHETICS[orderedTagId];
             if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
 
-            components.push(
-                <RoomSublist
-                    key={`sublist-${orderedTagId}`}
-                    tagId={orderedTagId}
-                    forRooms={true}
-                    startAsHidden={aesthetics.defaultHidden}
-                    label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
-                    onAddRoom={aesthetics.onAddRoom}
-                    addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
-                    addRoomContextMenu={aesthetics.addRoomContextMenu}
-                    isMinimized={this.props.isMinimized}
-                    onResize={this.props.onResize}
-                    extraBadTilesThatShouldntExist={extraTiles}
-                />
-            );
+            components.push(<RoomSublist
+                key={`sublist-${orderedTagId}`}
+                tagId={orderedTagId}
+                forRooms={true}
+                startAsHidden={aesthetics.defaultHidden}
+                label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
+                onAddRoom={aesthetics.onAddRoom}
+                addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
+                addRoomContextMenu={aesthetics.addRoomContextMenu}
+                isMinimized={this.props.isMinimized}
+                onResize={this.props.onResize}
+                extraBadTilesThatShouldntExist={extraTiles}
+            />);
         }
 
         return components;
diff --git a/src/components/views/rooms/RoomNameEditor.js b/src/components/views/rooms/RoomNameEditor.js
deleted file mode 100644
index e0c31321c3..0000000000
--- a/src/components/views/rooms/RoomNameEditor.js
+++ /dev/null
@@ -1,80 +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.
-*/
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import createReactClass from 'create-react-class';
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import * as sdk from "../../../index";
-import { _t } from '../../../languageHandler';
-
-export default createReactClass({
-    displayName: 'RoomNameEditor',
-
-    propTypes: {
-        room: PropTypes.object.isRequired,
-    },
-
-    getInitialState: function() {
-        return {
-            name: null,
-        };
-    },
-
-    // TODO: [REACT-WARNING] Move this to constructor
-    UNSAFE_componentWillMount: function() {
-        const room = this.props.room;
-        const name = room.currentState.getStateEvents('m.room.name', '');
-        const myId = MatrixClientPeg.get().credentials.userId;
-        const defaultName = room.getDefaultRoomName(myId);
-
-        this.setState({
-            name: name ? name.getContent().name : '',
-        });
-
-        this._placeholderName = _t("Unnamed Room");
-        if (defaultName && defaultName !== 'Empty room') { // default name from JS SDK, needs no translation as we don't ever show it.
-            this._placeholderName += " (" + defaultName + ")";
-        }
-    },
-
-    getRoomName: function() {
-        return this.state.name;
-    },
-
-    _onValueChanged: function(value, shouldSubmit) {
-        this.setState({
-            name: value,
-        });
-    },
-
-    render: function() {
-        const EditableText = sdk.getComponent("elements.EditableText");
-
-        return (
-                <div className="mx_RoomHeader_name">
-                    <EditableText
-                        className="mx_RoomHeader_nametext mx_RoomHeader_editable"
-                        placeholderClassName="mx_RoomHeader_placeholder"
-                        placeholder={this._placeholderName}
-                        blurToCancel={false}
-                        initialValue={this.state.name}
-                        onValueChanged={this._onValueChanged}
-                        dir="auto" />
-                </div>
-        );
-    },
-});
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js
index d52bbbb0d0..dc3893785d 100644
--- a/src/components/views/rooms/RoomPreviewBar.js
+++ b/src/components/views/rooms/RoomPreviewBar.js
@@ -26,6 +26,8 @@ import classNames from 'classnames';
 import { _t } from '../../../languageHandler';
 import SdkConfig from "../../../SdkConfig";
 import IdentityAuthClient from '../../../IdentityAuthClient';
+import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
+import {UPDATE_EVENT} from "../../../stores/AsyncStore";
 
 const MessageCase = Object.freeze({
     NotLoggedIn: "NotLoggedIn",
@@ -100,6 +102,7 @@ export default createReactClass({
 
     componentDidMount: function() {
         this._checkInvitedEmail();
+        CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate);
     },
 
     componentDidUpdate: function(prevProps, prevState) {
@@ -108,6 +111,10 @@ export default createReactClass({
         }
     },
 
+    componentWillUnmount: function() {
+        CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate);
+    },
+
     _checkInvitedEmail: async function() {
         // 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
@@ -143,6 +150,13 @@ export default createReactClass({
         }
     },
 
+    _onCommunityUpdate: function (roomId) {
+        if (this.props.room && this.props.room.roomId !== roomId) {
+            return;
+        }
+        this.forceUpdate(); // we have nothing to update
+    },
+
     _getMessageCase() {
         const isGuest = MatrixClientPeg.get().isGuest();
 
@@ -219,8 +233,15 @@ export default createReactClass({
         }
     },
 
+    _communityProfile: function() {
+        if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
+        return {displayName: null, avatarMxc: null};
+    },
+
     _roomName: function(atStart = false) {
-        const name = this.props.room ? this.props.room.name : this.props.roomAlias;
+        let name = this.props.room ? this.props.room.name : this.props.roomAlias;
+        const profile = this._communityProfile();
+        if (profile.displayName) name = profile.displayName;
         if (name) {
             return name;
         } else if (atStart) {
@@ -439,7 +460,10 @@ export default createReactClass({
             }
             case MessageCase.Invite: {
                 const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
-                const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
+                const oobData = Object.assign({}, this.props.oobData, {
+                    avatarUrl: this._communityProfile().avatarMxc,
+                });
+                const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
 
                 const inviteMember = this._getInviteMember();
                 let inviterElement;
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index 1e7ba3f77a..4056f2fbd4 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -517,15 +517,13 @@ export default class RoomSublist extends React.Component<IProps, IState> {
         if (this.state.rooms) {
             const visibleRooms = this.state.rooms.slice(0, this.numVisibleTiles);
             for (const room of visibleRooms) {
-                tiles.push(
-                    <RoomTile
-                        room={room}
-                        key={`room-${room.roomId}`}
-                        showMessagePreview={this.layout.showPreviews}
-                        isMinimized={this.props.isMinimized}
-                        tag={this.props.tagId}
-                    />
-                );
+                tiles.push(<RoomTile
+                    room={room}
+                    key={`room-${room.roomId}`}
+                    showMessagePreview={this.layout.showPreviews}
+                    isMinimized={this.props.isMinimized}
+                    tag={this.props.tagId}
+                />);
             }
         }
 
@@ -710,7 +708,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
                     // doesn't become sticky.
                     // The same applies to the notification badge.
                     return (
-                        <div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
+                        <div
+                            className={classes}
+                            onKeyDown={this.onHeaderKeyDown}
+                            onFocus={onFocus}
+                            aria-label={this.props.label}
+                        >
                             <div className="mx_RoomSublist_stickable">
                                 <Button
                                     onFocus={onFocus}
@@ -762,7 +765,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
             const showMoreAtMinHeight = minTiles < this.numTiles;
             const minHeightPadding = RESIZE_HANDLE_HEIGHT + (showMoreAtMinHeight ? SHOW_N_BUTTON_HEIGHT : 0);
             const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
-            let maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
+            const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
             const showMoreBtnClasses = classNames({
                 'mx_RoomSublist_showNButton': true,
             });
diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx
index 0c99b98e1a..a241a13991 100644
--- a/src/components/views/rooms/RoomTile.tsx
+++ b/src/components/views/rooms/RoomTile.tsx
@@ -27,11 +27,11 @@ import defaultDispatcher from '../../../dispatcher/dispatcher';
 import { Key } from "../../../Keyboard";
 import ActiveRoomObserver from "../../../ActiveRoomObserver";
 import { _t } from "../../../languageHandler";
-import { ChevronFace, ContextMenuTooltipButton, MenuItemRadio } from "../../structures/ContextMenu";
+import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
 import { DefaultTagID, TagID } from "../../../stores/room-list/models";
 import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
 import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
-import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE, } from "../../../RoomNotifs";
+import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import NotificationBadge from "./NotificationBadge";
 import { Volume } from "../../../RoomNotifsTypes";
@@ -47,8 +47,11 @@ import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber"
 import IconizedContextMenu, {
     IconizedContextMenuCheckbox,
     IconizedContextMenuOption,
-    IconizedContextMenuOptionList, IconizedContextMenuRadio
+    IconizedContextMenuOptionList,
+    IconizedContextMenuRadio,
 } from "../context_menus/IconizedContextMenu";
+import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
+import { UPDATE_EVENT } from "../../../stores/AsyncStore";
 
 interface IProps {
     room: Room;
@@ -101,6 +104,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
         this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
         this.roomProps = EchoChamber.forRoom(this.props.room);
         this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
+        CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
     }
 
     private onNotificationUpdate = () => {
@@ -140,6 +144,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
         defaultDispatcher.unregister(this.dispatcherRef);
         MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
         this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
+        CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
     }
 
     private onAction = (payload: ActionPayload) => {
@@ -150,6 +155,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
         }
     };
 
+    private onCommunityUpdate = (roomId: string) => {
+        if (roomId !== this.props.room.roomId) return;
+        this.forceUpdate(); // we don't have anything to actually update
+    };
+
     private onRoomPreviewChanged = (room: Room) => {
         if (this.props.room && room.roomId === this.props.room.roomId) {
             // generatePreview() will return nothing if the user has previews disabled
@@ -239,7 +249,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
                 removeTag,
                 addTag,
                 undefined,
-                0
+                0,
             ));
         } else {
             console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
@@ -461,11 +471,21 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
             'mx_RoomTile_minimized': this.props.isMinimized,
         });
 
+        let roomProfile: IRoomProfile = {displayName: null, avatarMxc: null};
+        if (this.props.tag === DefaultTagID.Invite) {
+            roomProfile = CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
+        }
+
+        let name = roomProfile.displayName || this.props.room.name;
+        if (typeof name !== 'string') name = '';
+        name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
+
         const roomAvatar = <DecoratedRoomAvatar
             room={this.props.room}
             avatarSize={32}
             tag={this.props.tag}
             displayBadge={this.props.isMinimized}
+            oobData={({avatarUrl: roomProfile.avatarMxc})}
         />;
 
         let badge: React.ReactNode;
@@ -482,10 +502,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
             );
         }
 
-        let name = this.props.room.name;
-        if (typeof name !== 'string') name = '';
-        name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
-
         let messagePreview = null;
         if (this.showMessagePreview && this.state.messagePreview) {
             messagePreview = (
diff --git a/src/components/views/rooms/RoomTopicEditor.js b/src/components/views/rooms/RoomTopicEditor.js
deleted file mode 100644
index 0adc0ff1d6..0000000000
--- a/src/components/views/rooms/RoomTopicEditor.js
+++ /dev/null
@@ -1,68 +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.
-*/
-
-import React from 'react';
-import PropTypes from 'prop-types';
-import createReactClass from 'create-react-class';
-import * as sdk from '../../../index';
-import { _t } from "../../../languageHandler";
-
-export default createReactClass({
-    displayName: 'RoomTopicEditor',
-
-    propTypes: {
-        room: PropTypes.object.isRequired,
-    },
-
-    getInitialState: function() {
-        return {
-            topic: null,
-        };
-    },
-
-    componentDidMount: function() {
-        const room = this.props.room;
-        const topic = room.currentState.getStateEvents('m.room.topic', '');
-        this.setState({
-            topic: topic ? topic.getContent().topic : '',
-        });
-    },
-
-    getTopic: function() {
-        return this.state.topic;
-    },
-
-    _onValueChanged: function(value) {
-        this.setState({
-            topic: value,
-        });
-    },
-
-    render: function() {
-        const EditableText = sdk.getComponent("elements.EditableText");
-
-        return (
-                <EditableText
-                     className="mx_RoomHeader_topic mx_RoomHeader_editable"
-                     placeholderClassName="mx_RoomHeader_placeholder"
-                     placeholder={_t("Add a topic")}
-                     blurToCancel={false}
-                     initialValue={this.state.topic}
-                     onValueChanged={this._onValueChanged}
-                     dir="auto" />
-        );
-    },
-});
diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx
index a61ff6e1da..eec3105880 100644
--- a/src/components/views/rooms/TemporaryTile.tsx
+++ b/src/components/views/rooms/TemporaryTile.tsx
@@ -19,9 +19,7 @@ import classNames from "classnames";
 import {
     RovingAccessibleButton,
     RovingAccessibleTooltipButton,
-    RovingTabIndexWrapper
 } from "../../../accessibility/RovingTabIndex";
-import AccessibleButton from "../../views/elements/AccessibleButton";
 import NotificationBadge from "./NotificationBadge";
 import { NotificationState } from "../../../stores/notifications/NotificationState";
 
diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js
index 1c6baee9af..847bcf3da3 100644
--- a/src/components/views/settings/CrossSigningPanel.js
+++ b/src/components/views/settings/CrossSigningPanel.js
@@ -89,6 +89,7 @@ export default class CrossSigningPanel extends React.PureComponent {
         const homeserverSupportsCrossSigning =
             await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
         const crossSigningReady = await cli.isCrossSigningReady();
+        const secretStorageReady = await cli.isSecretStorageReady();
 
         this.setState({
             crossSigningPublicKeysOnDevice,
@@ -101,6 +102,7 @@ export default class CrossSigningPanel extends React.PureComponent {
             secretStorageKeyInAccount,
             homeserverSupportsCrossSigning,
             crossSigningReady,
+            secretStorageReady,
         });
     }
 
@@ -151,6 +153,7 @@ export default class CrossSigningPanel extends React.PureComponent {
             secretStorageKeyInAccount,
             homeserverSupportsCrossSigning,
             crossSigningReady,
+            secretStorageReady,
         } = this.state;
 
         let errorSection;
@@ -166,14 +169,19 @@ export default class CrossSigningPanel extends React.PureComponent {
             summarisedStatus = <p>{_t(
                 "Your homeserver does not support cross-signing.",
             )}</p>;
-        } else if (crossSigningReady) {
+        } else if (crossSigningReady && secretStorageReady) {
             summarisedStatus = <p>✅ {_t(
-                "Cross-signing and secret storage are enabled.",
+                "Cross-signing and secret storage are ready for use.",
+            )}</p>;
+        } else if (crossSigningReady && !secretStorageReady) {
+            summarisedStatus = <p>✅ {_t(
+                "Cross-signing is ready for use, but secret storage is " +
+                "currently not being used to backup your keys.",
             )}</p>;
         } else if (crossSigningPrivateKeysInStorage) {
             summarisedStatus = <p>{_t(
-                "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.",
             )}</p>;
         } else {
             summarisedStatus = <p>{_t(
diff --git a/src/components/views/settings/UpdateCheckButton.tsx b/src/components/views/settings/UpdateCheckButton.tsx
index 10e0e29f31..eb1b762423 100644
--- a/src/components/views/settings/UpdateCheckButton.tsx
+++ b/src/components/views/settings/UpdateCheckButton.tsx
@@ -42,7 +42,7 @@ function getStatusText(status: UpdateCheckStatus, errorDetail?: string) {
             return _t('Downloading update...');
         case UpdateCheckStatus.Ready:
             return _t("New version available. <a>Update now.</a>", {}, {
-                a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{sub}</AccessibleButton>
+                a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{sub}</AccessibleButton>,
             });
     }
 }
diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
index c9ec4a6bc7..b4c05a2ecb 100644
--- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx
@@ -170,7 +170,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
             "baseFontSize",
             null,
             SettingLevel.DEVICE,
-            parseInt(value, 10) - FontWatcher.SIZE_DIFF
+            parseInt(value, 10) - FontWatcher.SIZE_DIFF,
         );
 
         return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
@@ -294,7 +294,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
                     />
                 </div>
                 {customThemeForm}
-           </div>
+            </div>
         );
     }
 
diff --git a/src/components/views/toasts/GenericExpiringToast.tsx b/src/components/views/toasts/GenericExpiringToast.tsx
index 83f43208c4..e63edd8e79 100644
--- a/src/components/views/toasts/GenericExpiringToast.tsx
+++ b/src/components/views/toasts/GenericExpiringToast.tsx
@@ -29,7 +29,15 @@ interface IProps extends IGenericToastProps {
 
 const SECOND = 1000;
 
-const GenericExpiringToast: React.FC<IProps> = ({description, acceptLabel, dismissLabel, onAccept, onDismiss, toastKey, numSeconds}) => {
+const GenericExpiringToast: React.FC<IProps> = ({
+    description,
+    acceptLabel,
+    dismissLabel,
+    onAccept,
+    onDismiss,
+    toastKey,
+    numSeconds,
+}) => {
     const onReject = () => {
         if (onDismiss) onDismiss();
         ToastStore.sharedInstance().dismissToast(toastKey);
diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx
index 6cd881b9eb..a9c64f1962 100644
--- a/src/components/views/toasts/GenericToast.tsx
+++ b/src/components/views/toasts/GenericToast.tsx
@@ -31,7 +31,13 @@ interface IPropsExtended extends IProps {
     onReject();
 }
 
-const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({description, acceptLabel, rejectLabel, onAccept, onReject}) => {
+const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
+    description,
+    acceptLabel,
+    rejectLabel,
+    onAccept,
+    onReject,
+}) => {
     return <div>
         <div className="mx_Toast_description">
             { description }
diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 8416f56fd9..1d3a62984a 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -97,10 +97,7 @@ export default class CallView extends React.Component<IProps, IState> {
         if (this.props.room) {
             const roomId = this.props.room.roomId;
             call = CallHandler.getCallForRoom(roomId) ||
-                (this.props.ConferenceHandler ?
-                 this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
-                 null
-                );
+                (this.props.ConferenceHandler ? this.props.ConferenceHandler.getConferenceCallForRoom(roomId) : null);
 
             if (this.call) {
                 this.setState({ call: call });
diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx
index 00d49b20f5..b7cba7a70f 100644
--- a/src/components/views/voip/IncomingCallBox.tsx
+++ b/src/components/views/voip/IncomingCallBox.tsx
@@ -51,7 +51,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
 
     private onAction = (payload: ActionPayload) => {
         switch (payload.action) {
-            case 'call_state':
+            case 'call_state': {
                 const call = CallHandler.getCall(payload.room_id);
                 if (call && call.call_state === 'ringing') {
                     this.setState({
@@ -62,6 +62,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
                         incomingCall: null,
                     });
                 }
+            }
         }
     };
 
diff --git a/src/createRoom.ts b/src/createRoom.ts
index 78d0cf1356..09de265ebc 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -27,6 +27,7 @@ import * as Rooms from "./Rooms";
 import DMRoomMap from "./utils/DMRoomMap";
 import {getAddressType} from "./UserAddress";
 import { getE2EEWellKnown } from "./utils/WellKnownUtils";
+import GroupStore from "./stores/GroupStore";
 
 // we define a number of interfaces which take their names from the js-sdk
 /* eslint-disable camelcase */
@@ -79,6 +80,7 @@ interface IOpts {
     encryption?: boolean;
     inlineErrors?: boolean;
     andView?: boolean;
+    associatedWithCommunity?: string;
 }
 
 /**
@@ -181,6 +183,10 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
         } else {
             return Promise.resolve();
         }
+    }).then(() => {
+        if (opts.associatedWithCommunity) {
+            return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false);
+        }
     }).then(function() {
         // NB createRoom doesn't block on the client seeing the echo that the
         // room has been created, so we race here with the client knowing that
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index c12b57c033..02382d4ff7 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -645,7 +645,8 @@
     "Confirm password": "Confirm password",
     "Change Password": "Change Password",
     "Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.",
-    "Cross-signing and secret storage are enabled.": "Cross-signing and secret storage are enabled.",
+    "Cross-signing and secret storage are ready for use.": "Cross-signing and secret storage are ready for use.",
+    "Cross-signing is ready for use, but secret storage is currently not being used to backup your keys.": "Cross-signing is ready for use, but secret storage is currently not being used to backup your keys.",
     "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.",
     "Cross-signing and secret storage are not yet set up.": "Cross-signing and secret storage are not yet set up.",
     "Reset cross-signing and secret storage": "Reset cross-signing and secret storage",
@@ -1121,6 +1122,7 @@
     "Rooms": "Rooms",
     "Add room": "Add room",
     "Create new room": "Create new room",
+    "Explore community rooms": "Explore community rooms",
     "Explore public rooms": "Explore public rooms",
     "Low priority": "Low priority",
     "System Alerts": "System Alerts",
@@ -1198,7 +1200,6 @@
     "%(count)s unread messages.|other": "%(count)s unread messages.",
     "%(count)s unread messages.|one": "1 unread message.",
     "Unread messages.": "Unread messages.",
-    "Add a topic": "Add a topic",
     "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.",
     "This room has already been upgraded.": "This room has already been upgraded.",
     "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.": "This room is running room version <roomVersion />, which this homeserver has marked as <i>unstable</i>.",
@@ -1484,6 +1485,7 @@
     "Rotate Right": "Rotate Right",
     "Rotate clockwise": "Rotate clockwise",
     "Download this file": "Download this file",
+    "Information": "Information",
     "Language Dropdown": "Language Dropdown",
     "Manage Integrations": "Manage Integrations",
     "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
@@ -1553,8 +1555,7 @@
     "Room directory": "Room directory",
     "Sign in with single sign-on": "Sign in with single sign-on",
     "And %(count)s more...|other": "And %(count)s more...",
-    "ex. @bob:example.com": "ex. @bob:example.com",
-    "Add User": "Add User",
+    "Home": "Home",
     "Enter a server name": "Enter a server name",
     "Looks good": "Looks good",
     "Can't find this server or its room list": "Can't find this server or its room list",
@@ -1597,6 +1598,15 @@
     "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s",
     "Unavailable": "Unavailable",
     "Changelog": "Changelog",
+    "Email address": "Email address",
+    "Add another email": "Add another email",
+    "People you know on %(brand)s": "People you know on %(brand)s",
+    "Hide": "Hide",
+    "Show": "Show",
+    "Skip": "Skip",
+    "Send %(count)s invites|other": "Send %(count)s invites",
+    "Send %(count)s invites|one": "Send %(count)s invite",
+    "Invite people to join %(communityName)s": "Invite people to join %(communityName)s",
     "You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)",
     "Removing…": "Removing…",
     "Destroy cross-signing keys?": "Destroy cross-signing keys?",
@@ -1607,6 +1617,15 @@
     "Clear all data in this session?": "Clear all data in this session?",
     "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.",
     "Clear all data": "Clear all data",
+    "There was an error creating your community. The name may be taken or the server is unable to process your request.": "There was an error creating your community. The name may be taken or the server is unable to process your request.",
+    "Community ID: +<localpart />:%(domain)s": "Community ID: +<localpart />:%(domain)s",
+    "Use this when referencing your community to others. The community ID cannot be changed.": "Use this when referencing your community to others. The community ID cannot be changed.",
+    "You can change this later if needed.": "You can change this later if needed.",
+    "What's the name of your community or team?": "What's the name of your community or team?",
+    "Enter name": "Enter name",
+    "Create": "Create",
+    "Add image (optional)": "Add image (optional)",
+    "An image will help people identify your community.": "An image will help people identify your community.",
     "Community IDs cannot be empty.": "Community IDs cannot be empty.",
     "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community IDs may only contain characters a-z, 0-9, or '=_-./'",
     "Something went wrong whilst creating your community": "Something went wrong whilst creating your community",
@@ -1615,20 +1634,22 @@
     "Example": "Example",
     "Community ID": "Community ID",
     "example": "example",
-    "Create": "Create",
     "Please enter a name for the room": "Please enter a name for the room",
-    "Set a room address to easily share your room with other people.": "Set a room address to easily share your room with other people.",
-    "This room is private, and can only be joined by invitation.": "This room is private, and can only be joined by invitation.",
+    "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.",
+    "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.",
     "You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.",
     "Enable end-to-end encryption": "Enable end-to-end encryption",
+    "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.",
+    "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.",
     "Create a public room": "Create a public room",
     "Create a private room": "Create a private room",
+    "Create a room in %(communityName)s": "Create a room in %(communityName)s",
     "Name": "Name",
     "Topic (optional)": "Topic (optional)",
     "Make this room public": "Make this room public",
     "Hide advanced": "Hide advanced",
     "Show advanced": "Show advanced",
-    "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)",
+    "Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
     "Create Room": "Create Room",
     "Sign out": "Sign out",
     "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this",
@@ -1774,9 +1795,7 @@
     "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "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": "Verification Pending",
     "Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.",
-    "Email address": "Email address",
     "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
-    "Skip": "Skip",
     "A username can only contain lower case letters, numbers and '=_-./'": "A username can only contain lower case letters, numbers and '=_-./'",
     "Username not available": "Username not available",
     "Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s",
@@ -1864,10 +1883,6 @@
     "<b>Warning</b>: You should only set up key backup from a trusted computer.": "<b>Warning</b>: You should only set up key backup from a trusted computer.",
     "Access your secure message history and set up secure messaging by entering your recovery key.": "Access your secure message history and set up secure messaging by entering your recovery key.",
     "If you've forgotten your recovery key you can <button>set up new recovery options</button>": "If you've forgotten your recovery key you can <button>set up new recovery options</button>",
-    "Private Chat": "Private Chat",
-    "Public Chat": "Public Chat",
-    "Custom": "Custom",
-    "Address (optional)": "Address (optional)",
     "Reject invitation": "Reject invitation",
     "Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
     "Unable to reject invite": "Unable to reject invite",
@@ -1889,7 +1904,6 @@
     "Set status": "Set status",
     "Set a new status...": "Set a new status...",
     "View Community": "View Community",
-    "Hide": "Hide",
     "Reload": "Reload",
     "Take picture": "Take picture",
     "Remove for everyone": "Remove for everyone",
@@ -1962,11 +1976,6 @@
     "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s",
     "Sign in to your Matrix account on <underlinedServerName />": "Sign in to your Matrix account on <underlinedServerName />",
     "Sign in with SSO": "Sign in with SSO",
-    "Sorry, your browser is <b>not</b> able to run %(brand)s.": "Sorry, your browser is <b>not</b> able to run %(brand)s.",
-    "%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.",
-    "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.": "Please install <chromeLink>Chrome</chromeLink>, <firefoxLink>Firefox</firefoxLink>, or <safariLink>Safari</safariLink> for the best experience.",
-    "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!",
-    "I understand the risks and wish to continue": "I understand the risks and wish to continue",
     "Couldn't load page": "Couldn't load page",
     "You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
     "You must join the room to see its files": "You must join the room to see its files",
@@ -2091,13 +2100,13 @@
     "Click to mute video": "Click to mute video",
     "Click to unmute audio": "Click to unmute audio",
     "Click to mute audio": "Click to mute audio",
+    "Create community": "Create community",
     "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
     "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
     "Failed to load timeline position": "Failed to load timeline position",
     "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
     "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
     "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
-    "Home": "Home",
     "Switch to light mode": "Switch to light mode",
     "Switch to dark mode": "Switch to dark mode",
     "Switch theme": "Switch theme",
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index 59edc8766c..d9feec95b1 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -442,7 +442,7 @@ export function pickBestLanguage(langs: string[]): string {
 }
 
 function getLangsJson(): Promise<object> {
-    return new Promise(async (resolve, reject) => {
+    return new Promise((resolve, reject) => {
         let url;
         if (typeof(webpackLangJsonUrl) === 'string') { // in Jest this 'url' isn't a URL, so just fall through
             url = webpackLangJsonUrl;
@@ -453,7 +453,7 @@ function getLangsJson(): Promise<object> {
             { method: "GET", url },
             (err, response, body) => {
                 if (err || response.status < 200 || response.status >= 300) {
-                    reject({err: err, response: response});
+                    reject(err);
                     return;
                 }
                 resolve(JSON.parse(body));
@@ -488,7 +488,7 @@ function getLanguage(langPath: string): object {
             { method: "GET", url: langPath },
             (err, response, body) => {
                 if (err || response.status < 200 || response.status >= 300) {
-                    reject({err: err, response: response});
+                    reject(err);
                     return;
                 }
                 resolve(weblateToCounterpart(JSON.parse(body)));
diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts
index 74292749b9..448562b68a 100644
--- a/src/rageshake/submit-rageshake.ts
+++ b/src/rageshake/submit-rageshake.ts
@@ -115,6 +115,7 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
             body.append("cross_signing_supported_by_hs",
                 String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")));
             body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
+            body.append("secret_storage_ready", String(await client.isSecretStorageReady()));
         }
     }
 
diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts
new file mode 100644
index 0000000000..581f8a97c8
--- /dev/null
+++ b/src/stores/CommunityPrototypeStore.ts
@@ -0,0 +1,100 @@
+/*
+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.
+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 { AsyncStoreWithClient } from "./AsyncStoreWithClient";
+import defaultDispatcher from "../dispatcher/dispatcher";
+import { ActionPayload } from "../dispatcher/payloads";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
+import SettingsStore from "../settings/SettingsStore";
+import * as utils from "matrix-js-sdk/src/utils";
+import { UPDATE_EVENT } from "./AsyncStore";
+
+interface IState {
+    // nothing of value - we use account data
+}
+
+export interface IRoomProfile {
+    displayName: string;
+    avatarMxc: string;
+}
+
+export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
+    private static internalInstance = new CommunityPrototypeStore();
+
+    private constructor() {
+        super(defaultDispatcher, {});
+    }
+
+    public static get instance(): CommunityPrototypeStore {
+        return CommunityPrototypeStore.internalInstance;
+    }
+
+    protected async onAction(payload: ActionPayload): Promise<any> {
+        if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) {
+            return;
+        }
+
+        if (payload.action === "MatrixActions.Room.myMembership") {
+            const room: Room = payload.room;
+            const membership = getEffectiveMembership(payload.membership);
+            const oldMembership = getEffectiveMembership(payload.oldMembership);
+            if (membership === oldMembership) return;
+
+            if (membership === EffectiveMembership.Invite) {
+                try {
+                    const path = utils.encodeUri("/rooms/$roomId/group_info", {$roomId: room.roomId});
+                    const profile = await this.matrixClient._http.authedRequest(
+                        undefined, "GET", path,
+                        undefined, undefined,
+                        {prefix: "/_matrix/client/unstable/im.vector.custom"});
+                    // we use global account data because per-room account data on invites is unreliable
+                    await this.matrixClient.setAccountData("im.vector.group_info." + room.roomId, profile);
+                } catch (e) {
+                    console.warn("Non-fatal error getting group information for invite:", e);
+                }
+            }
+        } else if (payload.action === "MatrixActions.accountData") {
+            if (payload.event_type.startsWith("im.vector.group_info.")) {
+                this.emit(UPDATE_EVENT, payload.event_type.substring("im.vector.group_info.".length));
+            }
+        }
+    }
+
+    public getInviteProfile(roomId: string): IRoomProfile {
+        if (!this.matrixClient) return {displayName: null, avatarMxc: null};
+        const room = this.matrixClient.getRoom(roomId);
+        if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
+            const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId);
+            if (data && data.getContent()) {
+                return {displayName: data.getContent().name, avatarMxc: data.getContent().avatar_url};
+            }
+        }
+        return {displayName: room.name, avatarMxc: room.avatar_url};
+    }
+
+    protected async onReady(): Promise<any> {
+        for (const room of this.matrixClient.getRooms()) {
+            const myMember = room.currentState.getMembers().find(m => m.userId === this.matrixClient.getUserId());
+            if (!myMember) continue;
+            if (getEffectiveMembership(myMember.membership) === EffectiveMembership.Invite) {
+                // Fake an update for anything that might have started listening before the invite
+                // data was available (eg: RoomPreviewBar after a refresh)
+                this.emit(UPDATE_EVENT, room.roomId);
+            }
+        }
+    }
+}
diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js
index 2b72a963b0..2eb35e6dc2 100644
--- a/src/stores/TagOrderStore.js
+++ b/src/stores/TagOrderStore.js
@@ -166,6 +166,25 @@ class TagOrderStore extends Store {
                     selectedTags: newTags,
                 });
 
+                if (!allowMultiple && newTags.length === 1) {
+                    // We're in prototype behaviour: select the general chat for the community
+                    const rooms = GroupStore.getGroupRooms(newTags[0])
+                        .map(r => MatrixClientPeg.get().getRoom(r.roomId))
+                        .filter(r => !!r);
+                    let chat = rooms.find(r => {
+                        const idState = r.currentState.getStateEvents("im.vector.general_chat", "");
+                        if (!idState || idState.getContent()['groupId'] !== newTags[0]) return false;
+                        return true;
+                    });
+                    if (!chat) chat = rooms[0];
+                    if (chat) {
+                        dis.dispatch({
+                            action: 'view_room',
+                            room_id: chat.roomId,
+                        });
+                    }
+                }
+
                 Analytics.trackEvent('FilterStore', 'select_tag');
             }
             break;
@@ -266,6 +285,13 @@ class TagOrderStore extends Store {
     getSelectedTags() {
         return this._state.selectedTags;
     }
+
+    getSelectedPrototypeTag() {
+        if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
+            return this.getSelectedTags()[0];
+        }
+        return null; // no selection as far as this function is concerned
+    }
 }
 
 if (global.singletonTagOrderStore === undefined) {