diff --git a/res/css/_components.scss b/res/css/_components.scss
index 389be11c60..7360c61c25 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -201,6 +201,7 @@
 @import "./views/rooms/_GroupLayout.scss";
 @import "./views/rooms/_IRCLayout.scss";
 @import "./views/rooms/_JumpToBottomButton.scss";
+@import "./views/rooms/_LinkPreviewGroup.scss";
 @import "./views/rooms/_LinkPreviewWidget.scss";
 @import "./views/rooms/_MemberInfo.scss";
 @import "./views/rooms/_MemberList.scss";
diff --git a/res/css/views/rooms/_LinkPreviewGroup.scss b/res/css/views/rooms/_LinkPreviewGroup.scss
new file mode 100644
index 0000000000..ed341904fd
--- /dev/null
+++ b/res/css/views/rooms/_LinkPreviewGroup.scss
@@ -0,0 +1,38 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_LinkPreviewGroup {
+    .mx_LinkPreviewGroup_hide {
+        cursor: pointer;
+        width: 18px;
+        height: 18px;
+
+        img {
+            flex: 0 0 40px;
+            visibility: hidden;
+        }
+    }
+
+    &:hover .mx_LinkPreviewGroup_hide img,
+    .mx_LinkPreviewGroup_hide.focus-visible:focus img {
+        visibility: visible;
+    }
+
+    > .mx_AccessibleButton {
+        color: $accent-color;
+        text-align: center;
+    }
+}
diff --git a/res/css/views/rooms/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss
index 022cf3ed28..e1628e19a6 100644
--- a/res/css/views/rooms/_LinkPreviewWidget.scss
+++ b/res/css/views/rooms/_LinkPreviewWidget.scss
@@ -33,12 +33,16 @@ limitations under the License.
 .mx_LinkPreviewWidget_caption {
     margin-left: 15px;
     flex: 1 1 auto;
+    overflow-x: hidden; // cause it to wrap rather than clip
 }
 
 .mx_LinkPreviewWidget_title {
-    display: inline;
     font-weight: bold;
     white-space: normal;
+    display: -webkit-box;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
 }
 
 .mx_LinkPreviewWidget_siteName {
@@ -49,22 +53,9 @@ limitations under the License.
     margin-top: 8px;
     white-space: normal;
     word-wrap: break-word;
-}
-
-.mx_LinkPreviewWidget_cancel {
-    cursor: pointer;
-    width: 18px;
-    height: 18px;
-
-    img {
-        flex: 0 0 40px;
-        visibility: hidden;
-    }
-}
-
-.mx_LinkPreviewWidget:hover .mx_LinkPreviewWidget_cancel img,
-.mx_LinkPreviewWidget_cancel.focus-visible:focus img {
-    visibility: visible;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical;
 }
 
 .mx_MatrixChat_useCompactLayout {
diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts
index d70585e5ec..d033063677 100644
--- a/src/DeviceListener.ts
+++ b/src/DeviceListener.ts
@@ -160,7 +160,8 @@ export default class DeviceListener {
         // which result in account data changes affecting checks below.
         if (
             ev.getType().startsWith('m.secret_storage.') ||
-            ev.getType().startsWith('m.cross_signing.')
+            ev.getType().startsWith('m.cross_signing.') ||
+            ev.getType() === 'm.megolm_backup.v1'
         ) {
             this._recheck();
         }
diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx
index d9e081341b..62de4dd2bb 100644
--- a/src/components/views/elements/RoomAliasField.tsx
+++ b/src/components/views/elements/RoomAliasField.tsx
@@ -27,6 +27,7 @@ interface IProps {
     value: string;
     label?: string;
     placeholder?: string;
+    disabled?: boolean;
     onChange?(value: string): void;
 }
 
@@ -68,6 +69,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState>
                 onChange={this.onChange}
                 value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
                 maxLength={maxlength}
+                disabled={this.props.disabled}
             />
         );
     }
diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx
index 6ba018c512..9c2786c642 100644
--- a/src/components/views/messages/TextualBody.tsx
+++ b/src/components/views/messages/TextualBody.tsx
@@ -45,7 +45,7 @@ import Spoiler from "../elements/Spoiler";
 import QuestionDialog from "../dialogs/QuestionDialog";
 import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
 import EditMessageComposer from '../rooms/EditMessageComposer';
-import LinkPreviewWidget from '../rooms/LinkPreviewWidget';
+import LinkPreviewGroup from '../rooms/LinkPreviewGroup';
 
 interface IProps {
     /* the MatrixEvent to show */
@@ -294,14 +294,8 @@ export default class TextualBody extends React.Component<IProps, IState> {
             // pass only the first child which is the event tile otherwise this recurses on edited events
             let links = this.findLinks([this.contentRef.current]);
             if (links.length) {
-                // de-duplicate the links after stripping hashes as they don't affect the preview
-                // using a set here maintains the order
-                links = Array.from(new Set(links.map(link => {
-                    const url = new URL(link);
-                    url.hash = "";
-                    return url.toString();
-                })));
-
+                // de-duplicate the links using a set here maintains the order
+                links = Array.from(new Set(links));
                 this.setState({ links });
 
                 // lazy-load the hidden state of the preview widget from localstorage
@@ -530,15 +524,12 @@ export default class TextualBody extends React.Component<IProps, IState> {
 
         let widgets;
         if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
-            widgets = this.state.links.map((link)=>{
-                return <LinkPreviewWidget
-                    key={link}
-                    link={link}
-                    mxEvent={this.props.mxEvent}
-                    onCancelClick={this.onCancelClick}
-                    onHeightChanged={this.props.onHeightChanged}
-                />;
-            });
+            widgets = <LinkPreviewGroup
+                links={this.state.links}
+                mxEvent={this.props.mxEvent}
+                onCancelClick={this.onCancelClick}
+                onHeightChanged={this.props.onHeightChanged}
+            />;
         }
 
         switch (content.msgtype) {
diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx
new file mode 100644
index 0000000000..ff6fd4afd2
--- /dev/null
+++ b/src/components/views/rooms/LinkPreviewGroup.tsx
@@ -0,0 +1,76 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useEffect } from "react";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
+import { useStateToggle } from "../../../hooks/useStateToggle";
+import LinkPreviewWidget from "./LinkPreviewWidget";
+import AccessibleButton from "../elements/AccessibleButton";
+import { _t } from "../../../languageHandler";
+
+const INITIAL_NUM_PREVIEWS = 2;
+
+interface IProps {
+    links: string[]; // the URLs to be previewed
+    mxEvent: MatrixEvent; // the Event associated with the preview
+    onCancelClick?(): void; // called when the preview's cancel ('hide') button is clicked
+    onHeightChanged?(): void; // called when the preview's contents has loaded
+}
+
+const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => {
+    const [expanded, toggleExpanded] = useStateToggle();
+    useEffect(() => {
+        onHeightChanged();
+    }, [onHeightChanged, expanded]);
+
+    const shownLinks = expanded ? links : links.slice(0, INITIAL_NUM_PREVIEWS);
+
+    let toggleButton;
+    if (links.length > INITIAL_NUM_PREVIEWS) {
+        toggleButton = <AccessibleButton onClick={toggleExpanded}>
+            { expanded
+                ? _t("Collapse")
+                : _t("Show %(count)s other previews", { count: links.length - shownLinks.length }) }
+        </AccessibleButton>;
+    }
+
+    return <div className="mx_LinkPreviewGroup">
+        { shownLinks.map((link, i) => (
+            <LinkPreviewWidget key={link} link={link} mxEvent={mxEvent} onHeightChanged={onHeightChanged}>
+                { i === 0 ? (
+                    <AccessibleButton
+                        className="mx_LinkPreviewGroup_hide"
+                        onClick={onCancelClick}
+                        aria-label={_t("Close preview")}
+                    >
+                        <img
+                            className="mx_filterFlipColor"
+                            alt=""
+                            role="presentation"
+                            src={require("../../../../res/img/cancel.svg")}
+                            width="18"
+                            height="18"
+                        />
+                    </AccessibleButton>
+                ): undefined }
+            </LinkPreviewWidget>
+        )) }
+        { toggleButton }
+    </div>;
+};
+
+export default LinkPreviewGroup;
diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.tsx
similarity index 73%
rename from src/components/views/rooms/LinkPreviewWidget.js
rename to src/components/views/rooms/LinkPreviewWidget.tsx
index 360ca41d55..db13021b32 100644
--- a/src/components/views/rooms/LinkPreviewWidget.js
+++ b/src/components/views/rooms/LinkPreviewWidget.tsx
@@ -1,6 +1,5 @@
 /*
-Copyright 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -16,26 +15,33 @@ limitations under the License.
 */
 
 import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
 import { AllHtmlEntities } from 'html-entities';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
+
 import { linkifyElement } from '../../../HtmlUtils';
 import SettingsStore from "../../../settings/SettingsStore";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
-import * as sdk from "../../../index";
 import Modal from "../../../Modal";
 import * as ImageUtils from "../../../ImageUtils";
-import { _t } from "../../../languageHandler";
 import { replaceableComponent } from "../../../utils/replaceableComponent";
 import { mediaFromMxc } from "../../../customisations/Media";
+import ImageView from '../elements/ImageView';
+
+interface IProps {
+    link: string; // the URL being previewed
+    mxEvent: MatrixEvent; // the Event associated with the preview
+    onHeightChanged(): void; // called when the preview's contents has loaded
+}
+
+interface IState {
+    preview?: IPreviewUrlResponse;
+}
 
 @replaceableComponent("views.rooms.LinkPreviewWidget")
-export default class LinkPreviewWidget extends React.Component {
-    static propTypes = {
-        link: PropTypes.string.isRequired, // the URL being previewed
-        mxEvent: PropTypes.object.isRequired, // the Event associated with the preview
-        onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked
-        onHeightChanged: PropTypes.func, // called when the preview's contents has loaded
-    };
+export default class LinkPreviewWidget extends React.Component<IProps, IState> {
+    private unmounted = false;
+    private readonly description = createRef<HTMLDivElement>();
 
     constructor(props) {
         super(props);
@@ -44,31 +50,25 @@ export default class LinkPreviewWidget extends React.Component {
             preview: null,
         };
 
-        this.unmounted = false;
-        MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{
+        MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((preview) => {
             if (this.unmounted) {
                 return;
             }
-            this.setState(
-                { preview: res },
-                this.props.onHeightChanged,
-            );
-        }, (error)=>{
+            this.setState({ preview }, this.props.onHeightChanged);
+        }, (error) => {
             console.error("Failed to get URL preview: " + error);
         });
-
-        this._description = createRef();
     }
 
     componentDidMount() {
-        if (this._description.current) {
-            linkifyElement(this._description.current);
+        if (this.description.current) {
+            linkifyElement(this.description.current);
         }
     }
 
     componentDidUpdate() {
-        if (this._description.current) {
-            linkifyElement(this._description.current);
+        if (this.description.current) {
+            linkifyElement(this.description.current);
         }
     }
 
@@ -76,11 +76,10 @@ export default class LinkPreviewWidget extends React.Component {
         this.unmounted = true;
     }
 
-    onImageClick = ev => {
+    private onImageClick = ev => {
         const p = this.state.preview;
         if (ev.button != 0 || ev.metaKey) return;
         ev.preventDefault();
-        const ImageView = sdk.getComponent("elements.ImageView");
 
         let src = p["og:image"];
         if (src && src.startsWith("mxc://")) {
@@ -136,21 +135,17 @@ export default class LinkPreviewWidget extends React.Component {
         // opaque string. This does not allow any HTML to be injected into the DOM.
         const description = AllHtmlEntities.decode(p["og:description"] || "");
 
-        const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
         return (
             <div className="mx_LinkPreviewWidget">
                 { img }
                 <div className="mx_LinkPreviewWidget_caption">
                     <div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
                     <div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
-                    <div className="mx_LinkPreviewWidget_description" ref={this._description}>
+                    <div className="mx_LinkPreviewWidget_description" ref={this.description}>
                         { description }
                     </div>
                 </div>
-                <AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
-                    <img className="mx_filterFlipColor" alt="" role="presentation"
-                        src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
-                </AccessibleButton>
+                { this.props.children }
             </div>
         );
     }
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 4bb61d7ccb..5f16684fb8 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -220,6 +220,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
                         value={alias}
                         placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
                         label={_t("Address")}
+                        disabled={busy}
                     />
                     : null
                 }
diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
index 3afdc629e4..9f4e0ecea7 100644
--- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
+++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
@@ -96,7 +96,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp
 
         { error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
 
-        <SpaceFeedbackPrompt onClick={() => onFinished(false)} />
+        <SpaceFeedbackPrompt />
 
         <div className="mx_SettingsTab_section">
             <SpaceBasicSettings
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index bbf6954435..7d4252545b 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1508,6 +1508,8 @@
     "Your message was sent": "Your message was sent",
     "Failed to send": "Failed to send",
     "Scroll to most recent messages": "Scroll to most recent messages",
+    "Show %(count)s other previews|other": "Show %(count)s other previews",
+    "Show %(count)s other previews|one": "Show %(count)s other preview",
     "Close preview": "Close preview",
     "and %(count)s others...|other": "and %(count)s others...",
     "and %(count)s others...|one": "and one other...",
diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js
index c9418fc557..c6a3f3c779 100644
--- a/test/components/views/messages/TextualBody-test.js
+++ b/test/components/views/messages/TextualBody-test.js
@@ -302,7 +302,7 @@ describe("<TextualBody />", () => {
             event: true,
         });
 
-        const wrapper = mount(<TextualBody mxEvent={ev} showUrlPreview={true} />);
+        const wrapper = mount(<TextualBody mxEvent={ev} showUrlPreview={true} onHeightChanged={() => {}} />);
         expect(wrapper.text()).toBe(ev.getContent().body);
 
         let widgets = wrapper.find("LinkPreviewWidget");