diff --git a/package.json b/package.json
index dbb6b29b53..4dc4bda3b2 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,6 @@
   "dependencies": {
     "@babel/runtime": "^7.12.5",
     "await-lock": "^2.1.0",
-    "blueimp-canvas-to-blob": "^3.28.0",
     "browser-encrypt-attachment": "^0.3.0",
     "browser-request": "^0.3.3",
     "cheerio": "^1.0.0-rc.9",
@@ -88,7 +87,6 @@
     "png-chunks-extract": "^1.0.0",
     "prop-types": "^15.7.2",
     "qrcode": "^1.4.4",
-    "qs": "^6.9.6",
     "re-resizable": "^6.9.0",
     "react": "^17.0.2",
     "react-beautiful-dnd": "^4.0.1",
@@ -99,7 +97,6 @@
     "rfc4648": "^1.4.0",
     "sanitize-html": "^2.3.2",
     "tar-js": "^0.3.0",
-    "text-encoding-utf-8": "^1.0.2",
     "url": "^0.11.0",
     "what-input": "^5.2.10",
     "zxcvbn": "^4.4.2"
diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 12762cbc9d..48b565be7f 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -328,7 +328,8 @@ $SpaceRoomViewInnerWidth: 428px;
             font-size: $font-15px;
             margin-top: 12px;
             margin-bottom: 16px;
-            white-space: pre;
+            white-space: pre-wrap;
+            word-wrap: break-word;
         }
 
         > hr {
diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss
index d8ff56663a..2e48b5d8e9 100644
--- a/res/css/views/dialogs/_InviteDialog.scss
+++ b/res/css/views/dialogs/_InviteDialog.scss
@@ -17,6 +17,9 @@ limitations under the License.
 .mx_InviteDialog_addressBar {
     display: flex;
     flex-direction: row;
+    // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
+    // for the user section gets weird.
+    margin: 8px 45px 0 0;
 
     .mx_InviteDialog_editor {
         flex: 1;
@@ -73,7 +76,7 @@ limitations under the License.
 }
 
 .mx_InviteDialog_section {
-    padding-bottom: 10px;
+    padding-bottom: 4px;
 
     h3 {
         font-size: $font-12px;
@@ -82,6 +85,14 @@ limitations under the License.
         text-transform: uppercase;
     }
 
+    > p {
+        margin: 0;
+    }
+
+    > span {
+        color: $primary-fg-color;
+    }
+
     .mx_InviteDialog_subname {
         margin-bottom: 10px;
         margin-top: -10px; // HACK: Positioning with margins is bad
@@ -90,6 +101,63 @@ limitations under the License.
     }
 }
 
+.mx_InviteDialog_section_hidden_suggestions_disclaimer {
+    padding: 8px 0 16px 0;
+    font-size: $font-14px;
+
+    > span {
+        color: $primary-fg-color;
+        font-weight: 600;
+    }
+
+    > p {
+        margin: 0;
+    }
+}
+
+.mx_InviteDialog_footer {
+    border-top: 1px solid $input-border-color;
+
+    > h3 {
+        margin: 12px 0;
+        font-size: $font-12px;
+        color: $muted-fg-color;
+        font-weight: bold;
+        text-transform: uppercase;
+    }
+
+    .mx_InviteDialog_footer_link {
+        display: flex;
+        justify-content: space-between;
+        border-radius: 4px;
+        border: solid 1px $light-fg-color;
+        padding: 8px;
+
+        > a {
+            text-decoration: none;
+            flex-shrink: 1;
+            overflow: hidden;
+            text-overflow: ellipsis;
+        }
+    }
+
+    .mx_InviteDialog_footer_link_copy {
+        flex-shrink: 0;
+        cursor: pointer;
+        margin-left: 20px;
+        display: inherit;
+
+        > div {
+            mask-image: url($copy-button-url);
+            background-color: $message-action-bar-fg-color;
+            margin-left: 5px;
+            width: 20px;
+            height: 20px;
+            background-repeat: no-repeat;
+        }
+    }
+}
+
 .mx_InviteDialog_roomTile {
     cursor: pointer;
     padding: 5px 10px;
@@ -142,6 +210,7 @@ limitations under the License.
 
     .mx_InviteDialog_roomTile_nameStack {
         display: inline-block;
+        overflow: hidden;
     }
 
     .mx_InviteDialog_roomTile_name {
@@ -157,6 +226,13 @@ limitations under the License.
         margin-left: 7px;
     }
 
+    .mx_InviteDialog_roomTile_name,
+    .mx_InviteDialog_roomTile_userId {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+    }
+
     .mx_InviteDialog_roomTile_time {
         text-align: right;
         font-size: $font-12px;
@@ -212,22 +288,29 @@ limitations under the License.
 
 .mx_InviteDialog {
     // Prevent the dialog from jumping around randomly when elements change.
-    height: 590px;
+    height: 600px;
     padding-left: 20px; // the design wants some padding on the left
+    display: flex;
+    flex-direction: column;
+
+    .mx_InviteDialog_content {
+        overflow: hidden;
+    }
 }
 
 .mx_InviteDialog_userSections {
-    margin-top: 10px;
+    margin-top: 4px;
     overflow-y: auto;
-    padding-right: 45px;
-    height: 455px; // mx_InviteDialog's height minus some for the upper elements
+    padding: 0 45px 4px 0;
+    height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements
 }
 
-// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar
-// for the user section gets weird.
-.mx_InviteDialog_helpText,
-.mx_InviteDialog_addressBar {
-    margin-right: 45px;
+.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections {
+    height: calc(100% - 175px);
+}
+
+.mx_InviteDialog_helpText {
+    margin: 0;
 }
 
 .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
diff --git a/res/css/views/messages/_SenderProfile.scss b/res/css/views/messages/_SenderProfile.scss
index 655cb39489..08644b14e3 100644
--- a/res/css/views/messages/_SenderProfile.scss
+++ b/res/css/views/messages/_SenderProfile.scss
@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-.mx_SenderProfile_name {
+.mx_SenderProfile_displayName {
     font-weight: 600;
 }
 
+.mx_SenderProfile_mxid {
+    font-weight: 600;
+    font-size: 1.1rem;
+    margin-left: 5px;
+    opacity: 0.5; // Match mx_TextualEvent
+}
diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss
index cf61ce569d..ae8d22a462 100644
--- a/res/css/views/rooms/_IRCLayout.scss
+++ b/res/css/views/rooms/_IRCLayout.scss
@@ -178,7 +178,7 @@ $irc-line-height: $font-18px;
         overflow: hidden;
         display: flex;
 
-        > .mx_SenderProfile_name {
+        > .mx_SenderProfile_displayName {
             overflow: hidden;
             text-overflow: ellipsis;
             min-width: var(--name-width);
@@ -207,7 +207,7 @@ $irc-line-height: $font-18px;
             background: transparent;
 
             > span {
-                > .mx_SenderProfile_name {
+                > .mx_SenderProfile_displayName {
                     min-width: inherit;
                 }
             }
diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx
index b21829ac63..9042d47243 100644
--- a/src/ContentMessages.tsx
+++ b/src/ContentMessages.tsx
@@ -28,8 +28,6 @@ import encrypt from "browser-encrypt-attachment";
 import extractPngChunks from "png-chunks-extract";
 import Spinner from "./components/views/elements/Spinner";
 
-// Polyfill for Canvas.toBlob API using Canvas.toDataURL
-import "blueimp-canvas-to-blob";
 import { Action } from "./dispatcher/actions";
 import CountlyAnalytics from "./CountlyAnalytics";
 import {
diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts
index 5545ed8483..f494c1eb22 100644
--- a/src/CountlyAnalytics.ts
+++ b/src/CountlyAnalytics.ts
@@ -24,13 +24,6 @@ import {sleep} from "./utils/promise";
 import RoomViewStore from "./stores/RoomViewStore";
 import { Action } from "./dispatcher/actions";
 
-// polyfill textencoder if necessary
-import * as TextEncodingUtf8 from 'text-encoding-utf-8';
-let TextEncoder = window.TextEncoder;
-if (!TextEncoder) {
-    TextEncoder = TextEncodingUtf8.TextEncoder;
-}
-
 const INACTIVITY_TIME = 20; // seconds
 const HEARTBEAT_INTERVAL = 5_000; // ms
 const SESSION_UPDATE_INTERVAL = 60; // seconds
diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index 8d59fe6c68..acbde0b097 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -520,6 +520,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
                             setError("Failed to update some suggestions. Try again later");
                         }
                         setSaving(false);
+                        setSelected(new Map());
                     }}
                     kind="primary_outline"
                     disabled={disabled}
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 276f4ae6ca..3ccf2e5424 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -28,7 +28,7 @@ import RoomTopic from "../views/elements/RoomTopic";
 import InlineSpinner from "../views/elements/InlineSpinner";
 import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
 import {useRoomMembers} from "../../hooks/useRoomMembers";
-import createRoom, {IOpts, Preset} from "../../createRoom";
+import createRoom, {IOpts} from "../../createRoom";
 import Field from "../views/elements/Field";
 import {useEventEmitter} from "../../hooks/useEventEmitter";
 import withValidation from "../views/elements/Validation";
@@ -65,6 +65,7 @@ import dis from "../../dispatcher/dispatcher";
 import Modal from "../../Modal";
 import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
 import SdkConfig from "../../SdkConfig";
+import { Preset } from "matrix-js-sdk/src/@types/partials";
 
 interface IProps {
     space: Room;
diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx
index cce6b6c34c..544f593708 100644
--- a/src/components/views/dialogs/CreateRoomDialog.tsx
+++ b/src/components/views/dialogs/CreateRoomDialog.tsx
@@ -15,22 +15,23 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {ChangeEvent, createRef, KeyboardEvent, SyntheticEvent} from "react";
-import {Room} from "matrix-js-sdk/src/models/room";
+import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
 
 import SdkConfig from '../../../SdkConfig';
-import withValidation, {IFieldState} from '../elements/Validation';
-import {_t} from '../../../languageHandler';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
-import {Key} from "../../../Keyboard";
-import {IOpts, Preset, privateShouldBeEncrypted, Visibility} from "../../../createRoom";
-import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import withValidation, { IFieldState } from '../elements/Validation';
+import { _t } from '../../../languageHandler';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import { Key } from "../../../Keyboard";
+import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
+import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
 import Field from "../elements/Field";
 import RoomAliasField from "../elements/RoomAliasField";
 import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
 import DialogButtons from "../elements/DialogButtons";
 import BaseDialog from "../dialogs/BaseDialog";
+import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
 
 interface IProps {
     defaultPublic?: boolean;
@@ -72,7 +73,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
             canChangeEncryption: true,
         };
 
-        MatrixClientPeg.get().doesServerForceEncryptionForPreset("private")
+        MatrixClientPeg.get().doesServerForceEncryptionForPreset(Preset.PrivateChat)
             .then(isForced => this.setState({ canChangeEncryption: !isForced }));
     }
 
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index b006205f11..778744b783 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {createRef} from 'react';
+import React, { createRef } from 'react';
+import classNames from 'classnames';
+
 import {_t, _td} from "../../../languageHandler";
 import * as sdk from "../../../index";
 import {MatrixClientPeg} from "../../../MatrixClientPeg";
@@ -31,7 +33,6 @@ import Modal from "../../../Modal";
 import {humanizeTime} from "../../../utils/humanize";
 import createRoom, {
     canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
-    IInvite3PID,
 } from "../../../createRoom";
 import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
 import {Key} from "../../../Keyboard";
@@ -50,6 +51,12 @@ import {getAddressType} from "../../../UserAddress";
 import BaseAvatar from '../avatars/BaseAvatar';
 import AccessibleButton from '../elements/AccessibleButton';
 import { compare } from '../../../utils/strings';
+import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
+import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
+import { copyPlaintext, selectText } from "../../../utils/strings";
+import * as ContextMenu from "../../structures/ContextMenu";
+import { toRightOf } from "../../structures/ContextMenu";
+import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
 
 // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
 /* eslint-disable camelcase */
@@ -351,6 +358,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         initialText: "",
     };
 
+    private closeCopiedTooltip: () => void;
     private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
     private editorRef = createRef<HTMLInputElement>();
     private unmounted = false;
@@ -403,6 +411,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 
     componentWillUnmount() {
         this.unmounted = true;
+        // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
+        // the tooltip otherwise, such as pressing Escape or clicking X really quickly
+        if (this.closeCopiedTooltip) this.closeCopiedTooltip();
     }
 
     private onConsultFirstChange = (ev) => {
@@ -1238,6 +1249,25 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
         }
     }
 
+    private async onLinkClick(e) {
+        e.preventDefault();
+        selectText(e.target);
+    }
+
+    private onCopyClick = async e => {
+        e.preventDefault();
+        const target = e.target; // copy target before we go async and React throws it away
+
+        const successful = await copyPlaintext(makeUserPermalink(MatrixClientPeg.get().getUserId()));
+        const buttonRect = target.getBoundingClientRect();
+        const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
+            ...toRightOf(buttonRect, 2),
+            message: successful ? _t("Copied!") : _t("Failed to copy"),
+        });
+        // Drop a reference to this close handler for componentWillUnmount
+        this.closeCopiedTooltip = target.onmouseleave = close;
+    };
+
     render() {
         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
         const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
@@ -1248,12 +1278,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             spinner = <Spinner w={20} h={20} />;
         }
 
-
         let title;
         let helpText;
         let buttonText;
         let goButtonFn;
-        let consultSection;
+        let extraSection;
+        let footer;
         let keySharingWarning = <span />;
 
         const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
@@ -1316,6 +1346,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             }
             buttonText = _t("Go");
             goButtonFn = this.startDm;
+            extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
+                <span>{ _t("Some suggestions may be hidden for privacy.") }</span>
+                <p>{ _t("If you can't see who you’re looking for, send them your invite link below.") }</p>
+            </div>;
+            const link = makeUserPermalink(MatrixClientPeg.get().getUserId());
+            footer = <div className="mx_InviteDialog_footer">
+                <h3>{ _t("Or send invite link") }</h3>
+                <div className="mx_InviteDialog_footer_link">
+                    <a href={link} onClick={this.onLinkClick}>
+                        { link }
+                    </a>
+                    <AccessibleTooltipButton
+                        title={_t("Copy")}
+                        onClick={this.onCopyClick}
+                        className="mx_InviteDialog_footer_link_copy"
+                    >
+                        <div />
+                    </AccessibleTooltipButton>
+                </div>
+            </div>
         } else if (this.props.kind === KIND_INVITE) {
             const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
             const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
@@ -1377,7 +1427,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             title = _t("Transfer");
             buttonText = _t("Transfer");
             goButtonFn = this.transferCall;
-            consultSection = <div>
+            footer = <div>
                 <label>
                     <input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
                     {_t("Consult first")}
@@ -1391,7 +1441,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
             || (this.state.filterText && this.state.filterText.includes('@'));
         return (
             <BaseDialog
-                className='mx_InviteDialog'
+                className={classNames("mx_InviteDialog", {
+                    mx_InviteDialog_hasFooter: !!footer,
+                })}
                 hasCancel={true}
                 onFinished={this.props.onFinished}
                 title={title}
@@ -1418,8 +1470,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                     <div className='mx_InviteDialog_userSections'>
                         {this.renderSection('recents')}
                         {this.renderSection('suggestions')}
+                        {extraSection}
                     </div>
-                    {consultSection}
+                    {footer}
                 </div>
             </BaseDialog>
         );
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index b15fbbed2b..cf3b7a6e61 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -128,6 +128,7 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
                 mxEvent={event}
                 layout={this.props.layout}
                 enableFlair={SettingsStore.getValue(UIFeature.Flair)}
+                as="div"
             />
         </div>;
     }
diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.tsx
similarity index 79%
rename from src/components/views/messages/SenderProfile.js
rename to src/components/views/messages/SenderProfile.tsx
index 8f10954370..805f842fbc 100644
--- a/src/components/views/messages/SenderProfile.js
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -15,24 +15,31 @@
  */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import Flair from '../elements/Flair.js';
 import FlairStore from '../../../stores/FlairStore';
 import {getUserNameColorClass} from '../../../utils/FormattingUtils';
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
+import MatrixEvent from "matrix-js-sdk/src/models/event";
+
+interface IProps {
+    mxEvent: MatrixEvent;
+    onClick(): void;
+    enableFlair: boolean;
+}
+
+interface IState {
+    userGroups;
+    relatedGroups;
+}
 
 @replaceableComponent("views.messages.SenderProfile")
-export default class SenderProfile extends React.Component {
-    static propTypes = {
-        mxEvent: PropTypes.object.isRequired, // event whose sender we're showing
-        onClick: PropTypes.func,
-    };
-
+export default class SenderProfile extends React.Component<IProps, IState> {
     static contextType = MatrixClientContext;
+    private unmounted: boolean;
 
-    constructor(props) {
-        super(props);
+    constructor(props: IProps) {
+        super(props)
         const senderId = this.props.mxEvent.getSender();
 
         this.state = {
@@ -40,6 +47,7 @@ export default class SenderProfile extends React.Component {
             relatedGroups: [],
         };
     }
+
     componentDidMount() {
         this.unmounted = false;
         this._updateRelatedGroups();
@@ -100,14 +108,26 @@ export default class SenderProfile extends React.Component {
     render() {
         const {mxEvent} = this.props;
         const colorClass = getUserNameColorClass(mxEvent.getSender());
-        const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
         const {msgtype} = mxEvent.getContent();
 
+        const disambiguate = mxEvent.sender?.disambiguate;
+        const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || "";
+        const mxid = mxEvent.sender?.userId || mxEvent.getSender() || "";
+
         if (msgtype === 'm.emote') {
             return null; // emote message must include the name so don't duplicate it
         }
 
-        let flair = null;
+        let mxidElement;
+        if (disambiguate) {
+            mxidElement = (
+                <span className="mx_SenderProfile_mxid">
+                    { mxid }
+                </span>
+            );
+        }
+
+        let flair;
         if (this.props.enableFlair) {
             const displayedGroups = this._getDisplayedGroups(
                 this.state.userGroups, this.state.relatedGroups,
@@ -119,13 +139,12 @@ export default class SenderProfile extends React.Component {
             />;
         }
 
-        const nameElem = name || '';
-
         return (
             <div className="mx_SenderProfile mx_SenderProfile_hover" dir="auto" onClick={this.props.onClick}>
-                <span className={`mx_SenderProfile_name ${colorClass}`}>
-                    { nameElem }
+                <span className={`mx_SenderProfile_displayName ${colorClass}`}>
+                    { displayName }
                 </span>
+                { mxidElement }
                 { flair }
             </div>
         );
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 0861f7fd5d..85b9cac2c4 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -1059,58 +1059,65 @@ export default class EventTile extends React.Component<IProps, IState> {
         switch (this.props.tileShape) {
             case 'notif': {
                 const room = this.context.getRoom(this.props.mxEvent.getRoomId());
-                return (
-                    <li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
-                        <div className="mx_EventTile_roomName">
-                            <RoomAvatar room={room} width={28} height={28} />
-                            <a href={permalink} onClick={this.onPermalinkClicked}>
-                                { room ? room.name : '' }
-                            </a>
-                        </div>
-                        <div className="mx_EventTile_senderDetails">
-                            { avatar }
-                            <a href={permalink} onClick={this.onPermalinkClicked}>
-                                { sender }
-                                { timestamp }
-                            </a>
-                        </div>
-                        <div className="mx_EventTile_line">
-                            <EventTileType ref={this.tile}
-                                mxEvent={this.props.mxEvent}
-                                highlights={this.props.highlights}
-                                highlightLink={this.props.highlightLink}
-                                showUrlPreview={this.props.showUrlPreview}
-                                onHeightChanged={this.props.onHeightChanged}
-                            />
-                        </div>
-                    </li>
-                );
+                return React.createElement(this.props.as || "li", {
+                    "className": classes,
+                    "aria-live": ariaLive,
+                    "aria-atomic": true,
+                    "data-scroll-tokens": scrollToken,
+                }, [
+                    <div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
+                        <RoomAvatar room={room} width={28} height={28} />
+                        <a href={permalink} onClick={this.onPermalinkClicked}>
+                            { room ? room.name : '' }
+                        </a>
+                    </div>,
+                    <div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
+                        { avatar }
+                        <a href={permalink} onClick={this.onPermalinkClicked}>
+                            { sender }
+                            { timestamp }
+                        </a>
+                    </div>,
+                    <div className="mx_EventTile_line" key="mx_EventTile_line">
+                        <EventTileType ref={this.tile}
+                            mxEvent={this.props.mxEvent}
+                            highlights={this.props.highlights}
+                            highlightLink={this.props.highlightLink}
+                            showUrlPreview={this.props.showUrlPreview}
+                            onHeightChanged={this.props.onHeightChanged}
+                        />
+                    </div>,
+                ]);
             }
             case 'file_grid': {
-                return (
-                    <li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
-                        <div className="mx_EventTile_line">
-                            <EventTileType ref={this.tile}
-                                mxEvent={this.props.mxEvent}
-                                highlights={this.props.highlights}
-                                highlightLink={this.props.highlightLink}
-                                showUrlPreview={this.props.showUrlPreview}
-                                tileShape={this.props.tileShape}
-                                onHeightChanged={this.props.onHeightChanged}
-                            />
+                return React.createElement(this.props.as || "li", {
+                    "className": classes,
+                    "aria-live": ariaLive,
+                    "aria-atomic": true,
+                    "data-scroll-tokens": scrollToken,
+                }, [
+                    <div className="mx_EventTile_line" key="mx_EventTile_line">
+                        <EventTileType ref={this.tile}
+                            mxEvent={this.props.mxEvent}
+                            highlights={this.props.highlights}
+                            highlightLink={this.props.highlightLink}
+                            showUrlPreview={this.props.showUrlPreview}
+                            tileShape={this.props.tileShape}
+                            onHeightChanged={this.props.onHeightChanged}
+                        />
+                    </div>,
+                    <a
+                        className="mx_EventTile_senderDetailsLink"
+                        key="mx_EventTile_senderDetailsLink"
+                        href={permalink}
+                        onClick={this.onPermalinkClicked}
+                    >
+                        <div className="mx_EventTile_senderDetails">
+                            { sender }
+                            { timestamp }
                         </div>
-                        <a
-                            className="mx_EventTile_senderDetailsLink"
-                            href={permalink}
-                            onClick={this.onPermalinkClicked}
-                        >
-                            <div className="mx_EventTile_senderDetails">
-                                { sender }
-                                { timestamp }
-                            </div>
-                        </a>
-                    </li>
-                );
+                    </a>,
+                ]);
             }
 
             case 'reply':
@@ -1126,27 +1133,30 @@ export default class EventTile extends React.Component<IProps, IState> {
                         this.props.alwaysShowTimestamps || this.state.hover,
                     );
                 }
-                return (
-                    <li className={classes} aria-live={ariaLive} aria-atomic="true" data-scroll-tokens={scrollToken}>
-                        { ircTimestamp }
-                        { avatar }
-                        { sender }
-                        { ircPadlock }
-                        <div className="mx_EventTile_reply">
-                            { groupTimestamp }
-                            { groupPadlock }
-                            { thread }
-                            <EventTileType ref={this.tile}
-                                mxEvent={this.props.mxEvent}
-                                highlights={this.props.highlights}
-                                highlightLink={this.props.highlightLink}
-                                onHeightChanged={this.props.onHeightChanged}
-                                replacingEventId={this.props.replacingEventId}
-                                showUrlPreview={false}
-                            />
-                        </div>
-                    </li>
-                );
+                return React.createElement(this.props.as || "li", {
+                    "className": classes,
+                    "aria-live": ariaLive,
+                    "aria-atomic": true,
+                    "data-scroll-tokens": scrollToken,
+                }, [
+                    ircTimestamp,
+                    avatar,
+                    sender,
+                    ircPadlock,
+                    <div className="mx_EventTile_reply" key="mx_EventTile_reply">
+                        { groupTimestamp }
+                        { groupPadlock }
+                        { thread }
+                        <EventTileType ref={this.tile}
+                            mxEvent={this.props.mxEvent}
+                            highlights={this.props.highlights}
+                            highlightLink={this.props.highlightLink}
+                            onHeightChanged={this.props.onHeightChanged}
+                            replacingEventId={this.props.replacingEventId}
+                            showUrlPreview={false}
+                        />
+                    </div>,
+                ]);
             }
             default: {
                 const thread = ReplyThread.makeThread(
diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js
index 5835eb9f36..c61424a059 100644
--- a/src/components/views/rooms/ReplyPreview.js
+++ b/src/components/views/rooms/ReplyPreview.js
@@ -95,6 +95,7 @@ export default class ReplyPreview extends React.Component {
                     permalinkCreator={this.props.permalinkCreator}
                     isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
                     enableFlair={SettingsStore.getValue(UIFeature.Flair)}
+                    as="div"
                 />
             </div>
         </div>;
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 0ebf511018..6a935ab276 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -22,7 +22,7 @@ import FocusLock from "react-focus-lock";
 import {_t} from "../../../languageHandler";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
-import createRoom, {IStateEvent, Preset} from "../../../createRoom";
+import createRoom from "../../../createRoom";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import {SpaceAvatar} from "./SpaceBasicSettings";
 import AccessibleButton from "../elements/AccessibleButton";
@@ -33,6 +33,8 @@ import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
 import Field from "../elements/Field";
 import withValidation from "../elements/Validation";
 import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
+import { Preset } from "matrix-js-sdk/src/@types/partials";
+import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
 
 const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
     return (
@@ -81,7 +83,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
             return;
         }
 
-        const initialState: IStateEvent[] = [
+        const initialState: ICreateRoomStateEvent[] = [
             {
                 type: EventType.RoomHistoryVisibility,
                 content: {
diff --git a/src/createRoom.ts b/src/createRoom.ts
index c6507b1380..2641492588 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -35,53 +35,15 @@ import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
 import SpaceStore from "./stores/SpaceStore";
 import { makeSpaceParentEvent } from "./utils/space";
 import { Action } from "./dispatcher/actions"
+import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
+import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
 
 // we define a number of interfaces which take their names from the js-sdk
 /* eslint-disable camelcase */
 
-// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
-export enum Visibility {
-    Public = "public",
-    Private = "private",
-}
-
-export enum Preset {
-    PrivateChat = "private_chat",
-    TrustedPrivateChat = "trusted_private_chat",
-    PublicChat = "public_chat",
-}
-
-interface Invite3PID {
-    id_server: string;
-    id_access_token?: string; // this gets injected by the js-sdk
-    medium: string;
-    address: string;
-}
-
-export interface IStateEvent {
-    type: string;
-    state_key?: string; // defaults to an empty string
-    content: object;
-}
-
-interface ICreateOpts {
-    visibility?: Visibility;
-    room_alias_name?: string;
-    name?: string;
-    topic?: string;
-    invite?: string[];
-    invite_3pid?: Invite3PID[];
-    room_version?: string;
-    creation_content?: object;
-    initial_state?: IStateEvent[];
-    preset?: Preset;
-    is_direct?: boolean;
-    power_level_content_override?: object;
-}
-
 export interface IOpts {
     dmUserId?: string;
-    createOpts?: ICreateOpts;
+    createOpts?: ICreateRoomOpts;
     spinner?: boolean;
     guestAccess?: boolean;
     encryption?: boolean;
@@ -91,12 +53,6 @@ export interface IOpts {
     parentSpace?: Room;
 }
 
-export interface IInvite3PID {
-    id_server: string,
-    medium: 'email',
-    address: string,
-}
-
 /**
  * Create a new room, and switch to it.
  *
@@ -136,7 +92,7 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
     const defaultPreset = opts.dmUserId ? Preset.TrustedPrivateChat : Preset.PrivateChat;
 
     // set some defaults for the creation
-    const createOpts = opts.createOpts || {};
+    const createOpts: ICreateRoomOpts = opts.createOpts || {};
     createOpts.preset = createOpts.preset || defaultPreset;
     createOpts.visibility = createOpts.visibility || Visibility.Private;
     if (opts.dmUserId && createOpts.invite === undefined) {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 874dc11bd2..17d6f64c46 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2257,6 +2257,9 @@
     "Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
     "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
     "Go": "Go",
+    "Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.",
+    "If you can't see who you’re looking for, send them your invite link below.": "If you can't see who you’re looking for, send them your invite link below.",
+    "Or send invite link": "Or send invite link",
     "Unnamed Space": "Unnamed Space",
     "Invite to %(roomName)s": "Invite to %(roomName)s",
     "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
diff --git a/src/mjolnir/Mjolnir.ts b/src/mjolnir/Mjolnir.ts
index 891438bbb9..f1fe816642 100644
--- a/src/mjolnir/Mjolnir.ts
+++ b/src/mjolnir/Mjolnir.ts
@@ -20,6 +20,7 @@ import SettingsStore from "../settings/SettingsStore";
 import {_t} from "../languageHandler";
 import dis from "../dispatcher/dispatcher";
 import {SettingLevel} from "../settings/SettingLevel";
+import { Preset } from "matrix-js-sdk/src/@types/partials";
 
 // TODO: Move this and related files to the js-sdk or something once finalized.
 
@@ -86,7 +87,7 @@ export class Mjolnir {
             const resp = await MatrixClientPeg.get().createRoom({
                 name: _t("My Ban List"),
                 topic: _t("This is your list of users/servers you have blocked - don't leave the room!"),
-                preset: "private_chat",
+                preset: Preset.PrivateChat,
             });
             personalRoomId = resp['room_id'];
             await SettingsStore.setValue(
diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts
index b2ad9fe6f6..08d8ccfd13 100644
--- a/src/rageshake/submit-rageshake.ts
+++ b/src/rageshake/submit-rageshake.ts
@@ -25,14 +25,8 @@ import Tar from "tar-js";
 
 import * as rageshake from './rageshake';
 
-// polyfill textencoder if necessary
-import * as TextEncodingUtf8 from 'text-encoding-utf-8';
 import SettingsStore from "../settings/SettingsStore";
 import SdkConfig from "../SdkConfig";
-let TextEncoder = window.TextEncoder;
-if (!TextEncoder) {
-    TextEncoder = TextEncodingUtf8.TextEncoder;
-}
 
 interface IOpts {
     label?: string;
diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js
index 6f5c7104b1..ae6b2999fa 100644
--- a/src/utils/MegolmExportEncryption.js
+++ b/src/utils/MegolmExportEncryption.js
@@ -15,17 +15,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-// polyfill textencoder if necessary
-import * as TextEncodingUtf8 from 'text-encoding-utf-8';
-let TextEncoder = window.TextEncoder;
-if (!TextEncoder) {
-    TextEncoder = TextEncodingUtf8.TextEncoder;
-}
-let TextDecoder = window.TextDecoder;
-if (!TextDecoder) {
-    TextDecoder = TextEncodingUtf8.TextDecoder;
-}
-
 import { _t } from '../languageHandler';
 import SdkConfig from '../SdkConfig';
 
diff --git a/test/end-to-end-tests/src/usecases/timeline.js b/test/end-to-end-tests/src/usecases/timeline.js
index 3889dce108..01dc618571 100644
--- a/test/end-to-end-tests/src/usecases/timeline.js
+++ b/test/end-to-end-tests/src/usecases/timeline.js
@@ -122,7 +122,7 @@ function getAllEventTiles(session) {
 }
 
 async function getMessageFromEventTile(eventTile) {
-    const senderElement = await eventTile.$(".mx_SenderProfile_name");
+    const senderElement = await eventTile.$(".mx_SenderProfile_displayName");
     const className = await (await eventTile.getProperty("className")).jsonValue();
     const classNames = className.split(" ");
     const bodyElement = await eventTile.$(".mx_EventTile_body");
diff --git a/test/setupTests.js b/test/setupTests.js
index 6d37d48987..9da412a7e8 100644
--- a/test/setupTests.js
+++ b/test/setupTests.js
@@ -1,6 +1,12 @@
 import * as languageHandler from "../src/languageHandler";
+import { TextEncoder, TextDecoder } from 'util';
 
 languageHandler.setLanguage('en');
 languageHandler.setMissingEntryGenerator(key => key.split("|", 2)[1]);
 
 require('jest-fetch-mock').enableMocks();
+
+// polyfilling TextEncoder as it is not available on JSDOM
+// view https://github.com/facebook/jest/issues/9983
+global.TextEncoder = TextEncoder;
+global.TextDecoder = TextDecoder;
diff --git a/yarn.lock b/yarn.lock
index 2b8667c7a6..f98cb5845e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2181,11 +2181,6 @@ bluebird@^3.5.0:
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
-blueimp-canvas-to-blob@^3.28.0:
-  version "3.28.0"
-  resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.28.0.tgz#c8ab4dc6bb08774a7f273798cdf94b0776adf6c8"
-  integrity sha512-5q+YHzgGsuHQ01iouGgJaPJXod2AzTxJXmVv90PpGrRxU7G7IqgPqWXz+PBmt3520jKKi6irWbNV87DicEa7wg==
-
 boolbase@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -7945,11 +7940,6 @@ test-exclude@^6.0.0:
     glob "^7.1.4"
     minimatch "^3.0.4"
 
-text-encoding-utf-8@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
-  integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
-
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"