diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a445a4041..c28d72a3eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,106 @@
+Changes in [3.31.0](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0) (2021-09-27)
+===================================================================================================
+
+## ✨ Features
+ * Say Joining space instead of Joining room where we know its a space ([\#6818](https://github.com/matrix-org/matrix-react-sdk/pull/6818)). Fixes vector-im/element-web#19064 and vector-im/element-web#19064.
+ * Add warning that some spaces may not be relinked to the newly upgraded room ([\#6805](https://github.com/matrix-org/matrix-react-sdk/pull/6805)). Fixes vector-im/element-web#18858 and vector-im/element-web#18858.
+ * Delabs Spaces, iterate some copy and move communities/space toggle to preferences ([\#6594](https://github.com/matrix-org/matrix-react-sdk/pull/6594)). Fixes vector-im/element-web#18088, vector-im/element-web#18524 vector-im/element-web#18088 and vector-im/element-web#18088.
+ * Show "Message" in the user info panel instead of "Start chat" ([\#6319](https://github.com/matrix-org/matrix-react-sdk/pull/6319)). Fixes vector-im/element-web#17877 and vector-im/element-web#17877. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix space keyboard shortcuts conflicting with native zoom shortcuts ([\#6804](https://github.com/matrix-org/matrix-react-sdk/pull/6804)).
+ * Replace plain text emoji at the end of a line ([\#6784](https://github.com/matrix-org/matrix-react-sdk/pull/6784)). Fixes vector-im/element-web#18833 and vector-im/element-web#18833. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Simplify Space Panel layout and fix some edge cases ([\#6800](https://github.com/matrix-org/matrix-react-sdk/pull/6800)). Fixes vector-im/element-web#18694 and vector-im/element-web#18694.
+ * Show unsent message warning on Space Panel buttons ([\#6778](https://github.com/matrix-org/matrix-react-sdk/pull/6778)). Fixes vector-im/element-web#18891 and vector-im/element-web#18891.
+ * Hide mute/unmute button in UserInfo for Spaces as it makes no sense ([\#6790](https://github.com/matrix-org/matrix-react-sdk/pull/6790)). Fixes vector-im/element-web#19007 and vector-im/element-web#19007.
+ * Fix automatic field population in space create menu not validating ([\#6792](https://github.com/matrix-org/matrix-react-sdk/pull/6792)). Fixes vector-im/element-web#19005 and vector-im/element-web#19005.
+ * Optimize input label transition on focus ([\#6783](https://github.com/matrix-org/matrix-react-sdk/pull/6783)). Fixes vector-im/element-web#12876 and vector-im/element-web#12876. Contributed by [MadLittleMods](https://github.com/MadLittleMods).
+ * Adapt and re-use the RolesRoomSettingsTab for Spaces ([\#6779](https://github.com/matrix-org/matrix-react-sdk/pull/6779)). Fixes vector-im/element-web#18908 vector-im/element-web#18909 and vector-im/element-web#18908.
+ * Deduplicate join rule management between rooms and spaces ([\#6724](https://github.com/matrix-org/matrix-react-sdk/pull/6724)). Fixes vector-im/element-web#18798 and vector-im/element-web#18798.
+ * Add config option to turn on in-room event sending timing metrics ([\#6766](https://github.com/matrix-org/matrix-react-sdk/pull/6766)).
+ * Improve the upgrade for restricted user experience ([\#6764](https://github.com/matrix-org/matrix-react-sdk/pull/6764)). Fixes vector-im/element-web#18677 and vector-im/element-web#18677.
+ * Improve tooltips on space quick actions and explore button ([\#6760](https://github.com/matrix-org/matrix-react-sdk/pull/6760)). Fixes vector-im/element-web#18528 and vector-im/element-web#18528.
+ * Make space members and user info behave more expectedly ([\#6765](https://github.com/matrix-org/matrix-react-sdk/pull/6765)). Fixes vector-im/element-web#17018 and vector-im/element-web#17018.
+ * hide no-op m.room.encryption events and better word param changes ([\#6747](https://github.com/matrix-org/matrix-react-sdk/pull/6747)). Fixes vector-im/element-web#18597 and vector-im/element-web#18597.
+ * Respect m.space.parent relations if they hold valid permissions ([\#6746](https://github.com/matrix-org/matrix-react-sdk/pull/6746)). Fixes vector-im/element-web#10935 and vector-im/element-web#10935.
+ * Space panel accessibility improvements ([\#6744](https://github.com/matrix-org/matrix-react-sdk/pull/6744)). Fixes vector-im/element-web#18892 and vector-im/element-web#18892.
+
+## 🐛 Bug Fixes
+ * Fix spacing for message composer buttons ([\#6854](https://github.com/matrix-org/matrix-react-sdk/pull/6854)).
+ * Fix accessing field on oobData which may be undefined ([\#6830](https://github.com/matrix-org/matrix-react-sdk/pull/6830)). Fixes vector-im/element-web#19085 and vector-im/element-web#19085.
+ * Fix reactions aria-label not being a string and thus being read as [Object object] ([\#6828](https://github.com/matrix-org/matrix-react-sdk/pull/6828)).
+ * Fix missing null guard in space hierarchy pagination ([\#6821](https://github.com/matrix-org/matrix-react-sdk/pull/6821)). Fixes matrix-org/element-web-rageshakes#6299 and matrix-org/element-web-rageshakes#6299.
+ * Fix checks to show prompt to start new chats ([\#6812](https://github.com/matrix-org/matrix-react-sdk/pull/6812)).
+ * Fix room list scroll jumps ([\#6777](https://github.com/matrix-org/matrix-react-sdk/pull/6777)). Fixes vector-im/element-web#17460 vector-im/element-web#18440 and vector-im/element-web#17460. Contributed by [robintown](https://github.com/robintown).
+ * Fix various message bubble alignment issues ([\#6785](https://github.com/matrix-org/matrix-react-sdk/pull/6785)). Fixes vector-im/element-web#18293, vector-im/element-web#18294 vector-im/element-web#18305 and vector-im/element-web#18293. Contributed by [robintown](https://github.com/robintown).
+ * Make message bubble font size consistent ([\#6795](https://github.com/matrix-org/matrix-react-sdk/pull/6795)). Contributed by [robintown](https://github.com/robintown).
+ * Fix edge cases around joining new room which does not belong to active space ([\#6797](https://github.com/matrix-org/matrix-react-sdk/pull/6797)). Fixes vector-im/element-web#19025 and vector-im/element-web#19025.
+ * Fix edge case space issues around creation and initial view ([\#6798](https://github.com/matrix-org/matrix-react-sdk/pull/6798)). Fixes vector-im/element-web#19023 and vector-im/element-web#19023.
+ * Stop spinner on space preview if the join fails ([\#6803](https://github.com/matrix-org/matrix-react-sdk/pull/6803)). Fixes vector-im/element-web#19034 and vector-im/element-web#19034.
+ * Fix emoji picker and stickerpicker not appearing correctly when opened ([\#6793](https://github.com/matrix-org/matrix-react-sdk/pull/6793)). Fixes vector-im/element-web#19012 and vector-im/element-web#19012. Contributed by [Palid](https://github.com/Palid).
+ * Fix autocomplete not having y-scroll ([\#6794](https://github.com/matrix-org/matrix-react-sdk/pull/6794)). Fixes vector-im/element-web#18997 and vector-im/element-web#18997. Contributed by [Palid](https://github.com/Palid).
+ * Fix broken edge case with public space creation with no alias ([\#6791](https://github.com/matrix-org/matrix-react-sdk/pull/6791)). Fixes vector-im/element-web#19003 and vector-im/element-web#19003.
+ * Redirect from /#/welcome to /#/home if already logged in ([\#6786](https://github.com/matrix-org/matrix-react-sdk/pull/6786)). Fixes vector-im/element-web#18990 and vector-im/element-web#18990. Contributed by [aaronraimist](https://github.com/aaronraimist).
+ * Fix build issues from two conflicting PRs landing without merge conflict ([\#6780](https://github.com/matrix-org/matrix-react-sdk/pull/6780)).
+ * Render guest settings only in public rooms/spaces ([\#6693](https://github.com/matrix-org/matrix-react-sdk/pull/6693)). Fixes vector-im/element-web#18776 and vector-im/element-web#18776. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix message bubble corners being wrong in the presence of hidden events ([\#6776](https://github.com/matrix-org/matrix-react-sdk/pull/6776)). Fixes vector-im/element-web#18124 and vector-im/element-web#18124. Contributed by [robintown](https://github.com/robintown).
+ * Debounce read marker update on scroll ([\#6771](https://github.com/matrix-org/matrix-react-sdk/pull/6771)). Fixes vector-im/element-web#18961 and vector-im/element-web#18961.
+ * Use cursor:pointer on space panel buttons ([\#6770](https://github.com/matrix-org/matrix-react-sdk/pull/6770)). Fixes vector-im/element-web#18951 and vector-im/element-web#18951.
+ * Fix regressed tab view buttons in space update toast ([\#6761](https://github.com/matrix-org/matrix-react-sdk/pull/6761)). Fixes vector-im/element-web#18781 and vector-im/element-web#18781.
+
+Changes in [3.31.0-rc.2](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0-rc.2) (2021-09-22)
+=============================================================================================================
+
+## 🐛 Bug Fixes
+ * Fix spacing for message composer buttons ([\#6854](https://github.com/matrix-org/matrix-react-sdk/pull/6854)).
+
+Changes in [3.31.0-rc.1](https://github.com/vector-im/element-desktop/releases/tag/v3.31.0-rc.1) (2021-09-21)
+=============================================================================================================
+
+## ✨ Features
+ * Say Joining space instead of Joining room where we know its a space ([\#6818](https://github.com/matrix-org/matrix-react-sdk/pull/6818)). Fixes vector-im/element-web#19064 and vector-im/element-web#19064.
+ * Add warning that some spaces may not be relinked to the newly upgraded room ([\#6805](https://github.com/matrix-org/matrix-react-sdk/pull/6805)). Fixes vector-im/element-web#18858 and vector-im/element-web#18858.
+ * Delabs Spaces, iterate some copy and move communities/space toggle to preferences ([\#6594](https://github.com/matrix-org/matrix-react-sdk/pull/6594)). Fixes vector-im/element-web#18088, vector-im/element-web#18524 vector-im/element-web#18088 and vector-im/element-web#18088.
+ * Show "Message" in the user info panel instead of "Start chat" ([\#6319](https://github.com/matrix-org/matrix-react-sdk/pull/6319)). Fixes vector-im/element-web#17877 and vector-im/element-web#17877. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix space keyboard shortcuts conflicting with native zoom shortcuts ([\#6804](https://github.com/matrix-org/matrix-react-sdk/pull/6804)).
+ * Replace plain text emoji at the end of a line ([\#6784](https://github.com/matrix-org/matrix-react-sdk/pull/6784)). Fixes vector-im/element-web#18833 and vector-im/element-web#18833. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Simplify Space Panel layout and fix some edge cases ([\#6800](https://github.com/matrix-org/matrix-react-sdk/pull/6800)). Fixes vector-im/element-web#18694 and vector-im/element-web#18694.
+ * Show unsent message warning on Space Panel buttons ([\#6778](https://github.com/matrix-org/matrix-react-sdk/pull/6778)). Fixes vector-im/element-web#18891 and vector-im/element-web#18891.
+ * Hide mute/unmute button in UserInfo for Spaces as it makes no sense ([\#6790](https://github.com/matrix-org/matrix-react-sdk/pull/6790)). Fixes vector-im/element-web#19007 and vector-im/element-web#19007.
+ * Fix automatic field population in space create menu not validating ([\#6792](https://github.com/matrix-org/matrix-react-sdk/pull/6792)). Fixes vector-im/element-web#19005 and vector-im/element-web#19005.
+ * Optimize input label transition on focus ([\#6783](https://github.com/matrix-org/matrix-react-sdk/pull/6783)). Fixes vector-im/element-web#12876 and vector-im/element-web#12876. Contributed by [MadLittleMods](https://github.com/MadLittleMods).
+ * Adapt and re-use the RolesRoomSettingsTab for Spaces ([\#6779](https://github.com/matrix-org/matrix-react-sdk/pull/6779)). Fixes vector-im/element-web#18908 vector-im/element-web#18909 and vector-im/element-web#18908.
+ * Deduplicate join rule management between rooms and spaces ([\#6724](https://github.com/matrix-org/matrix-react-sdk/pull/6724)). Fixes vector-im/element-web#18798 and vector-im/element-web#18798.
+ * Add config option to turn on in-room event sending timing metrics ([\#6766](https://github.com/matrix-org/matrix-react-sdk/pull/6766)).
+ * Improve the upgrade for restricted user experience ([\#6764](https://github.com/matrix-org/matrix-react-sdk/pull/6764)). Fixes vector-im/element-web#18677 and vector-im/element-web#18677.
+ * Improve tooltips on space quick actions and explore button ([\#6760](https://github.com/matrix-org/matrix-react-sdk/pull/6760)). Fixes vector-im/element-web#18528 and vector-im/element-web#18528.
+ * Make space members and user info behave more expectedly ([\#6765](https://github.com/matrix-org/matrix-react-sdk/pull/6765)). Fixes vector-im/element-web#17018 and vector-im/element-web#17018.
+ * hide no-op m.room.encryption events and better word param changes ([\#6747](https://github.com/matrix-org/matrix-react-sdk/pull/6747)). Fixes vector-im/element-web#18597 and vector-im/element-web#18597.
+ * Respect m.space.parent relations if they hold valid permissions ([\#6746](https://github.com/matrix-org/matrix-react-sdk/pull/6746)). Fixes vector-im/element-web#10935 and vector-im/element-web#10935.
+ * Space panel accessibility improvements ([\#6744](https://github.com/matrix-org/matrix-react-sdk/pull/6744)). Fixes vector-im/element-web#18892 and vector-im/element-web#18892.
+
+## 🐛 Bug Fixes
+ * Revert Firefox composer deletion hacks ([\#6844](https://github.com/matrix-org/matrix-react-sdk/pull/6844)). Fixes vector-im/element-web#19103 and vector-im/element-web#19103. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix accessing field on oobData which may be undefined ([\#6830](https://github.com/matrix-org/matrix-react-sdk/pull/6830)). Fixes vector-im/element-web#19085 and vector-im/element-web#19085.
+ * Fix pill deletion on Firefox 78 ([\#6832](https://github.com/matrix-org/matrix-react-sdk/pull/6832)). Fixes vector-im/element-web#19077 and vector-im/element-web#19077. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix reactions aria-label not being a string and thus being read as [Object object] ([\#6828](https://github.com/matrix-org/matrix-react-sdk/pull/6828)).
+ * Fix missing null guard in space hierarchy pagination ([\#6821](https://github.com/matrix-org/matrix-react-sdk/pull/6821)). Fixes matrix-org/element-web-rageshakes#6299 and matrix-org/element-web-rageshakes#6299.
+ * Fix checks to show prompt to start new chats ([\#6812](https://github.com/matrix-org/matrix-react-sdk/pull/6812)).
+ * Fix room list scroll jumps ([\#6777](https://github.com/matrix-org/matrix-react-sdk/pull/6777)). Fixes vector-im/element-web#17460 vector-im/element-web#18440 and vector-im/element-web#17460. Contributed by [robintown](https://github.com/robintown).
+ * Fix various message bubble alignment issues ([\#6785](https://github.com/matrix-org/matrix-react-sdk/pull/6785)). Fixes vector-im/element-web#18293, vector-im/element-web#18294 vector-im/element-web#18305 and vector-im/element-web#18293. Contributed by [robintown](https://github.com/robintown).
+ * Make message bubble font size consistent ([\#6795](https://github.com/matrix-org/matrix-react-sdk/pull/6795)). Contributed by [robintown](https://github.com/robintown).
+ * Fix edge cases around joining new room which does not belong to active space ([\#6797](https://github.com/matrix-org/matrix-react-sdk/pull/6797)). Fixes vector-im/element-web#19025 and vector-im/element-web#19025.
+ * Fix edge case space issues around creation and initial view ([\#6798](https://github.com/matrix-org/matrix-react-sdk/pull/6798)). Fixes vector-im/element-web#19023 and vector-im/element-web#19023.
+ * Stop spinner on space preview if the join fails ([\#6803](https://github.com/matrix-org/matrix-react-sdk/pull/6803)). Fixes vector-im/element-web#19034 and vector-im/element-web#19034.
+ * Fix emoji picker and stickerpicker not appearing correctly when opened ([\#6793](https://github.com/matrix-org/matrix-react-sdk/pull/6793)). Fixes vector-im/element-web#19012 and vector-im/element-web#19012. Contributed by [Palid](https://github.com/Palid).
+ * Fix autocomplete not having y-scroll ([\#6794](https://github.com/matrix-org/matrix-react-sdk/pull/6794)). Fixes vector-im/element-web#18997 and vector-im/element-web#18997. Contributed by [Palid](https://github.com/Palid).
+ * Fix broken edge case with public space creation with no alias ([\#6791](https://github.com/matrix-org/matrix-react-sdk/pull/6791)). Fixes vector-im/element-web#19003 and vector-im/element-web#19003.
+ * Redirect from /#/welcome to /#/home if already logged in ([\#6786](https://github.com/matrix-org/matrix-react-sdk/pull/6786)). Fixes vector-im/element-web#18990 and vector-im/element-web#18990. Contributed by [aaronraimist](https://github.com/aaronraimist).
+ * Fix build issues from two conflicting PRs landing without merge conflict ([\#6780](https://github.com/matrix-org/matrix-react-sdk/pull/6780)).
+ * Render guest settings only in public rooms/spaces ([\#6693](https://github.com/matrix-org/matrix-react-sdk/pull/6693)). Fixes vector-im/element-web#18776 and vector-im/element-web#18776. Contributed by [SimonBrandner](https://github.com/SimonBrandner).
+ * Fix message bubble corners being wrong in the presence of hidden events ([\#6776](https://github.com/matrix-org/matrix-react-sdk/pull/6776)). Fixes vector-im/element-web#18124 and vector-im/element-web#18124. Contributed by [robintown](https://github.com/robintown).
+ * Debounce read marker update on scroll ([\#6771](https://github.com/matrix-org/matrix-react-sdk/pull/6771)). Fixes vector-im/element-web#18961 and vector-im/element-web#18961.
+ * Use cursor:pointer on space panel buttons ([\#6770](https://github.com/matrix-org/matrix-react-sdk/pull/6770)). Fixes vector-im/element-web#18951 and vector-im/element-web#18951.
+ * Fix regressed tab view buttons in space update toast ([\#6761](https://github.com/matrix-org/matrix-react-sdk/pull/6761)). Fixes vector-im/element-web#18781 and vector-im/element-web#18781.
+
 Changes in [3.30.0](https://github.com/vector-im/element-desktop/releases/tag/v3.30.0) (2021-09-14)
 ===================================================================================================
 
diff --git a/package.json b/package.json
index 3e3d9383c4..89084acd68 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "matrix-react-sdk",
-  "version": "3.30.0",
+  "version": "3.31.0",
   "description": "SDK for matrix.org using React",
   "author": "matrix.org",
   "repository": {
diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 86c2efeb4a..fd9c4a14fc 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -89,7 +89,6 @@ limitations under the License.
     margin: 0px auto;
 
     overflow: auto;
-    flex: 0 0 auto;
 }
 
 .mx_RoomView_auxPanel_fullHeight {
diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss
index 032cb49359..e19be82e25 100644
--- a/res/css/views/elements/_ReplyThread.scss
+++ b/res/css/views/elements/_ReplyThread.scss
@@ -59,3 +59,14 @@ limitations under the License.
         border-left-color: $username-variant8-color;
     }
 }
+
+.mx_ReplyThread--expanded {
+    .mx_EventTile_body {
+        display: block;
+        overflow-y: scroll !important;
+    }
+    .mx_EventTile_collapsedCodeBlock {
+        // !important needed due to .mx_ReplyTile .mx_EventTile_content .mx_EventTile_pre_container > pre
+        display: block !important;
+    }
+}
diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss
index b9d845ea7a..1043fd08d1 100644
--- a/res/css/views/elements/_RichText.scss
+++ b/res/css/views/elements/_RichText.scss
@@ -18,7 +18,7 @@ a.mx_Pill {
     text-overflow: ellipsis;
     white-space: nowrap;
     overflow: hidden;
-    max-width: calc(100% - 1ch);
+    max-width: 100%;
 }
 
 .mx_Pill {
diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss
index 6805036e3d..46fc11956f 100644
--- a/res/css/views/messages/_MessageActionBar.scss
+++ b/res/css/views/messages/_MessageActionBar.scss
@@ -117,6 +117,16 @@ limitations under the License.
     mask-image: url('$(res)/img/download.svg');
 }
 
+.mx_MessageActionBar_expandMessageButton::after {
+    mask-size: 12px;
+    mask-image: url('$(res)/img/element-icons/expand-message.svg');
+}
+
+.mx_MessageActionBar_collapseMessageButton::after {
+    mask-size: 12px;
+    mask-image: url('$(res)/img/element-icons/collapse-message.svg');
+}
+
 .mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
     background-color: transparent; // hide the download icon mask
 }
diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss
index 3f526a6bba..7084c2f20e 100644
--- a/res/css/views/spaces/_SpaceCreateMenu.scss
+++ b/res/css/views/spaces/_SpaceCreateMenu.scss
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-$spacePanelWidth: 71px;
+$spacePanelWidth: 68px;
 
 .mx_SpaceCreateMenu_wrapper {
     // background blur everything except SpacePanel
diff --git a/res/img/element-icons/collapse-message.svg b/res/img/element-icons/collapse-message.svg
new file mode 100644
index 0000000000..91b0713f43
--- /dev/null
+++ b/res/img/element-icons/collapse-message.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#737D8C" fill-rule="evenodd" d="M.2192.234A.753.753 0 011.2815.2321l3.7243 3.7003L8.7181.2202A.753.753 0 019.7805.2185a.747.747 0 01.0017 1.0589L5.5396 5.52a.753.753 0 01-1.0624.0018L.221 1.2928A.747.747 0 01.2192.234zM9.7822 13.7663a.7529.7529 0 01-1.0623.0017l-3.7243-3.7003L1.2833 13.78a.753.753 0 01-1.0624.0018.7471.7471 0 01-.0017-1.059l4.2426-4.2426a.753.753 0 011.0624-.0017l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/res/img/element-icons/expand-message.svg b/res/img/element-icons/expand-message.svg
new file mode 100644
index 0000000000..a1c5149718
--- /dev/null
+++ b/res/img/element-icons/expand-message.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 14"><defs/><path fill="#17191C" fill-rule="evenodd" d="M.2192 8.494a.753.753 0 011.0623-.0018l3.7243 3.7003 3.7123-3.7123a.753.753 0 011.0624-.0017.747.747 0 01.0017 1.059L5.5396 13.78a.753.753 0 01-1.0624.0018L.221 9.5528A.747.747 0 01.2192 8.494zM9.7822 5.5063A.753.753 0 018.72 5.508L4.9956 1.8077 1.2833 5.52a.753.753 0 01-1.0624.0018.747.747 0 01-.0017-1.059L4.4618.2202A.753.753 0 015.5242.2185l4.2563 4.2289a.747.747 0 01.0017 1.0589z" clip-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/res/img/element-icons/message/view-in-timeline.svg b/res/img/element-icons/message/view-in-timeline.svg
new file mode 100644
index 0000000000..9f05950ce0
--- /dev/null
+++ b/res/img/element-icons/message/view-in-timeline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><path fill="#737D8C" fill-rule="evenodd" d="M1 2.75A.75.75 0 0 1 1.75 2h.005a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75Zm2.495 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.005a.75.75 0 0 1 0 1.5h-.005a.75.75 0 0 1-.75-.75ZM1 6.75A.75.75 0 0 1 1.75 6h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 1 6.75ZM1 9.75A.75.75 0 0 1 1.75 9h4.5a.75.75 0 0 1 0 1.5h-4.5A.75.75 0 0 1 1 9.75ZM1 13.75a.75.75 0 0 1 .75-.75h.005a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1-.75-.75Zm2.495 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.01a.75.75 0 0 1 0 1.5h-.01a.75.75 0 0 1-.75-.75Zm2.5 0a.75.75 0 0 1 .75-.75h.005a.75.75 0 0 1 0 1.5h-.005a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd"/></svg>
\ No newline at end of file
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index d65f8e3a10..2173230627 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -45,7 +45,7 @@ function getOrCreateContainer(): HTMLDivElement {
 
 const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
 
-interface IPosition {
+export interface IPosition {
     top?: number;
     bottom?: number;
     left?: number;
@@ -430,7 +430,11 @@ export type AboveLeftOf = IPosition & {
 
 // Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
 // and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
-export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0): AboveLeftOf => {
+export const aboveLeftOf = (
+    elementRect: DOMRect,
+    chevronFace = ChevronFace.None,
+    vPadding = 0,
+): AboveLeftOf => {
     const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
 
     const buttonRight = elementRect.right + window.pageXOffset;
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 743928c272..15bf327a74 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -78,7 +78,6 @@ import { objectHasDiff } from "../../utils/objects";
 import SpaceRoomView from "./SpaceRoomView";
 import { IOpts } from "../../createRoom";
 import { replaceableComponent } from "../../utils/replaceableComponent";
-import UIStore from "../../stores/UIStore";
 import EditorStateTransfer from "../../utils/EditorStateTransfer";
 import { throttle } from "lodash";
 import ErrorDialog from '../views/dialogs/ErrorDialog';
@@ -158,7 +157,6 @@ export interface IState {
     // used by componentDidUpdate to avoid unnecessary checks
     atEndOfLiveTimelineInit: boolean;
     showTopUnreadMessagesBar: boolean;
-    auxPanelMaxHeight?: number;
     statusBarVisible: boolean;
     // We load this later by asking the js-sdk to suggest a version for us.
     // This object is the result of Room#getRecommendedVersion()
@@ -565,10 +563,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         });
 
         window.addEventListener('beforeunload', this.onPageUnload);
-        if (this.props.resizeNotifier) {
-            this.props.resizeNotifier.on("middlePanelResized", this.onResize);
-        }
-        this.onResize();
     }
 
     shouldComponentUpdate(nextProps, nextState) {
@@ -656,9 +650,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         }
 
         window.removeEventListener('beforeunload', this.onPageUnload);
-        if (this.props.resizeNotifier) {
-            this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
-        }
 
         // Remove RoomStore listener
         if (this.roomStoreToken) {
@@ -1619,28 +1610,6 @@ export default class RoomView extends React.Component<IProps, IState> {
         };
     }
 
-    private onResize = () => {
-        // It seems flexbox doesn't give us a way to constrain the auxPanel height to have
-        // a minimum of the height of the video element, whilst also capping it from pushing out the page
-        // so we have to do it via JS instead.  In this implementation we cap the height by putting
-        // a maxHeight on the underlying remote video tag.
-
-        // header + footer + status + give us at least 120px of scrollback at all times.
-        let auxPanelMaxHeight = UIStore.instance.windowHeight -
-                (54 + // height of RoomHeader
-                 36 + // height of the status area
-                 51 + // minimum height of the message composer
-                 120); // amount of desired scrollback
-
-        // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
-        // but it's better than the video going missing entirely
-        if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
-
-        if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) {
-            this.setState({ auxPanelMaxHeight });
-        }
-    };
-
     private onStatusBarVisible = () => {
         if (this.unmounted || this.state.statusBarVisible) return;
         this.setState({ statusBarVisible: true });
@@ -1941,11 +1910,8 @@ export default class RoomView extends React.Component<IProps, IState> {
         const auxPanel = (
             <AuxPanel
                 room={this.state.room}
-                fullHeight={false}
                 userId={this.context.credentials.userId}
-                maxHeight={this.state.auxPanelMaxHeight}
                 showApps={this.state.showApps}
-                onResize={this.onResize}
                 resizeNotifier={this.props.resizeNotifier}
             >
                 { aux }
diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx
index bb31c32877..180a870cd5 100644
--- a/src/components/structures/ThreadView.tsx
+++ b/src/components/structures/ThreadView.tsx
@@ -139,7 +139,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
                         sendReadReceiptOnLoad={false} // No RR support in thread's MVP
                         timelineSet={this.state?.thread?.timelineSet}
                         showUrlPreview={true}
-                        tileShape={TileShape.Notif}
+                        tileShape={TileShape.Thread}
                         empty={<div>empty</div>}
                         alwaysShowTimestamps={true}
                         layout={Layout.Group}
diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx
index 8f5d3baa17..22dd3ac438 100644
--- a/src/components/views/context_menus/MessageContextMenu.tsx
+++ b/src/components/views/context_menus/MessageContextMenu.tsx
@@ -34,10 +34,10 @@ import ForwardDialog from "../dialogs/ForwardDialog";
 import { Action } from "../../../dispatcher/actions";
 import ReportEventDialog from '../dialogs/ReportEventDialog';
 import ViewSource from '../../structures/ViewSource';
-import ConfirmRedactDialog from '../dialogs/ConfirmRedactDialog';
-import ErrorDialog from '../dialogs/ErrorDialog';
+import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
 import ShareDialog from '../dialogs/ShareDialog';
 import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
+import { IPosition, ChevronFace } from '../../structures/ContextMenu';
 
 export function canCancel(eventStatus: EventStatus): boolean {
     return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@@ -52,7 +52,8 @@ export interface IOperableEventTile {
     getEventTileOps(): IEventTileOps;
 }
 
-interface IProps {
+interface IProps extends IPosition {
+    chevronFace: ChevronFace;
     /* the MatrixEvent associated with the context menu */
     mxEvent: MatrixEvent;
     /* an optional EventTileOps implementation that can be used to unhide preview widgets */
@@ -138,34 +139,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
     };
 
     private onRedactClick = (): void => {
-        Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
-            onFinished: async (proceed: boolean, reason?: string) => {
-                if (!proceed) return;
-
-                const cli = MatrixClientPeg.get();
-                try {
-                    this.props.onCloseDialog?.();
-                    await cli.redactEvent(
-                        this.props.mxEvent.getRoomId(),
-                        this.props.mxEvent.getId(),
-                        undefined,
-                        reason ? { reason } : {},
-                    );
-                } catch (e) {
-                    const code = e.errcode || e.statusCode;
-                    // only show the dialog if failing for something other than a network error
-                    // (e.g. no errcode or statusCode) as in that case the redactions end up in the
-                    // detached queue and we show the room status bar to allow retry
-                    if (typeof code !== "undefined") {
-                        // display error message stating you couldn't delete this.
-                        Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
-                            title: _t('Error'),
-                            description: _t('You cannot delete this message. (%(code)s)', { code }),
-                        });
-                    }
-                }
-            },
-        }, 'mx_Dialog_confirmredact');
+        const { mxEvent, onCloseDialog } = this.props;
+        createRedactEventDialog({
+            mxEvent,
+            onCloseDialog,
+        });
         this.closeMenu();
     };
 
diff --git a/src/components/views/dialogs/ConfirmRedactDialog.tsx b/src/components/views/dialogs/ConfirmRedactDialog.tsx
index b346d2d44c..74b3320fdf 100644
--- a/src/components/views/dialogs/ConfirmRedactDialog.tsx
+++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx
@@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
 import React from 'react';
 import { _t } from '../../../languageHandler';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import Modal from '../../../Modal';
 import { replaceableComponent } from "../../../utils/replaceableComponent";
+import ErrorDialog from './ErrorDialog';
 import TextInputDialog from "./TextInputDialog";
 
 interface IProps {
@@ -42,3 +46,40 @@ export default class ConfirmRedactDialog extends React.Component<IProps> {
         );
     }
 }
+
+export function createRedactEventDialog({
+    mxEvent,
+    onCloseDialog = () => {},
+}: {
+    mxEvent: MatrixEvent;
+    onCloseDialog?: () => void;
+}) {
+    Modal.createTrackedDialog('Confirm Redact Dialog', '', ConfirmRedactDialog, {
+        onFinished: async (proceed: boolean, reason?: string) => {
+            if (!proceed) return;
+
+            const cli = MatrixClientPeg.get();
+            try {
+                onCloseDialog?.();
+                await cli.redactEvent(
+                    mxEvent.getRoomId(),
+                    mxEvent.getId(),
+                    undefined,
+                    reason ? { reason } : {},
+                );
+            } catch (e) {
+                const code = e.errcode || e.statusCode;
+                // only show the dialog if failing for something other than a network error
+                // (e.g. no errcode or statusCode) as in that case the redactions end up in the
+                // detached queue and we show the room status bar to allow retry
+                if (typeof code !== "undefined") {
+                    // display error message stating you couldn't delete this.
+                    Modal.createTrackedDialog('You cannot delete this message', '', ErrorDialog, {
+                        title: _t('Error'),
+                        description: _t('You cannot delete this message. (%(code)s)', { code }),
+                    });
+                }
+            }
+        },
+    }, 'mx_Dialog_confirmredact');
+}
diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx
index 485dfe8ff2..23a56eadae 100644
--- a/src/components/views/dialogs/LeaveSpaceDialog.tsx
+++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx
@@ -30,8 +30,13 @@ interface IProps {
 }
 
 const isOnlyAdmin = (room: Room): boolean => {
-    return !room.getJoinedMembers().some(member => {
-        return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100;
+    const userId = room.client.getUserId();
+    if (room.getMember(userId).powerLevelNorm !== 100) {
+        return false; // user is not an admin
+    }
+    return room.getJoinedMembers().every(member => {
+        // return true if every other member has a lower power level (we are highest)
+        return member.userId === userId || member.powerLevelNorm < 100;
     });
 };
 
diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx
index 59c827d5d8..bd81218623 100644
--- a/src/components/views/elements/ReplyThread.tsx
+++ b/src/components/views/elements/ReplyThread.tsx
@@ -16,6 +16,8 @@ limitations under the License.
 */
 
 import React from 'react';
+import classNames from 'classnames';
+
 import { _t } from '../../../languageHandler';
 import dis from '../../../dispatcher/dispatcher';
 import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
@@ -35,6 +37,12 @@ import ReplyTile from "../rooms/ReplyTile";
 import Pill from './Pill';
 import { Room } from 'matrix-js-sdk/src/models/room';
 
+/**
+ * This number is based on the previous behavior - if we have message of height
+ * over 60px then we want to show button that will allow to expand it.
+ */
+const SHOW_EXPAND_QUOTE_PIXELS = 60;
+
 interface IProps {
     // the latest event in this chain of replies
     parentEv?: MatrixEvent;
@@ -45,6 +53,8 @@ interface IProps {
     layout?: Layout;
     // Whether to always show a timestamp
     alwaysShowTimestamps?: boolean;
+    isQuoteExpanded?: boolean;
+    setQuoteExpanded: (isExpanded: boolean) => void;
 }
 
 interface IState {
@@ -66,6 +76,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
     static contextType = MatrixClientContext;
     private unmounted = false;
     private room: Room;
+    private blockquoteRef = React.createRef<HTMLElement>();
 
     constructor(props, context) {
         super(props, context);
@@ -80,7 +91,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
         this.room = this.context.getRoom(this.props.parentEv.getRoomId());
     }
 
-    public static getParentEventId(ev: MatrixEvent): string {
+    public static getParentEventId(ev: MatrixEvent): string | undefined {
         if (!ev || ev.isRedacted()) return;
 
         // XXX: For newer relations (annotations, replacements, etc.), we now
@@ -137,7 +148,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
     public static getNestedReplyText(
         ev: MatrixEvent,
         permalinkCreator: RoomPermalinkCreator,
-    ): { body: string, html: string } {
+    ): { body: string, html: string } | null {
         if (!ev) return null;
 
         let { body, formatted_body: html } = ev.getContent();
@@ -237,37 +248,38 @@ export default class ReplyThread extends React.Component<IProps, IState> {
         return replyMixin;
     }
 
-    public static makeThread(
-        parentEv: MatrixEvent,
-        onHeightChanged: () => void,
-        permalinkCreator: RoomPermalinkCreator,
-        ref: React.RefObject<ReplyThread>,
-        layout: Layout,
-        alwaysShowTimestamps: boolean,
-    ): JSX.Element {
-        if (!ReplyThread.getParentEventId(parentEv)) return null;
-        return <ReplyThread
-            parentEv={parentEv}
-            onHeightChanged={onHeightChanged}
-            ref={ref}
-            permalinkCreator={permalinkCreator}
-            layout={layout}
-            alwaysShowTimestamps={alwaysShowTimestamps}
-        />;
+    public static hasThreadReply(event: MatrixEvent) {
+        return Boolean(ReplyThread.getParentEventId(event));
     }
 
     componentDidMount() {
         this.initialize();
+        this.trySetExpandableQuotes();
     }
 
     componentDidUpdate() {
         this.props.onHeightChanged();
+        this.trySetExpandableQuotes();
     }
 
     componentWillUnmount() {
         this.unmounted = true;
     }
 
+    private trySetExpandableQuotes() {
+        if (this.props.isQuoteExpanded === undefined && this.blockquoteRef.current) {
+            const el: HTMLElement | null = this.blockquoteRef.current.querySelector('.mx_EventTile_body');
+            if (el) {
+                const code: HTMLElement | null = el.querySelector('code');
+                const isCodeEllipsisShown = code ? code.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS : false;
+                const isElipsisShown = el.offsetHeight >= SHOW_EXPAND_QUOTE_PIXELS || isCodeEllipsisShown;
+                if (isElipsisShown) {
+                    this.props.setQuoteExpanded(false);
+                }
+            }
+        }
+    }
+
     private async initialize(): Promise<void> {
         const { parentEv } = this.props;
         // at time of making this component we checked that props.parentEv has a parentEventId
@@ -321,7 +333,7 @@ export default class ReplyThread extends React.Component<IProps, IState> {
         this.initialize();
     };
 
-    private onQuoteClick = async (): Promise<void> => {
+    private onQuoteClick = async (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>): Promise<void> => {
         const events = [this.state.loadedEv, ...this.state.events];
 
         let loadedEv = null;
@@ -373,14 +385,26 @@ export default class ReplyThread extends React.Component<IProps, IState> {
             header = <Spinner w={16} h={16} />;
         }
 
+        const { isQuoteExpanded } = this.props;
         const evTiles = this.state.events.map((ev) => {
-            return <blockquote className={`mx_ReplyThread ${this.getReplyThreadColorClass(ev)}`} key={ev.getId()}>
-                <ReplyTile
-                    mxEvent={ev}
-                    onHeightChanged={this.props.onHeightChanged}
-                    permalinkCreator={this.props.permalinkCreator}
-                />
-            </blockquote>;
+            const classname = classNames({
+                'mx_ReplyThread': true,
+                [this.getReplyThreadColorClass(ev)]: true,
+                // We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
+                'mx_ReplyThread--expanded': isQuoteExpanded === true,
+                // We don't want to add the class if it's undefined, it should only be expanded/collapsed when it's true/false
+                'mx_ReplyThread--collapsed': isQuoteExpanded === false,
+            });
+            return (
+                <blockquote ref={this.blockquoteRef} className={classname} key={ev.getId()}>
+                    <ReplyTile
+                        mxEvent={ev}
+                        onHeightChanged={this.props.onHeightChanged}
+                        permalinkCreator={this.props.permalinkCreator}
+                        toggleExpandedQuote={() => this.props.setQuoteExpanded(!this.props.isQuoteExpanded)}
+                    />
+                </blockquote>
+            );
         });
 
         return <div className="mx_ReplyThread_wrapper">
diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx
index f76fa32ddc..06817b910a 100644
--- a/src/components/views/messages/MessageActionBar.tsx
+++ b/src/components/views/messages/MessageActionBar.tsx
@@ -17,7 +17,8 @@ limitations under the License.
 */
 
 import React, { useEffect } from 'react';
-import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
+import { EventStatus, MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import type { Relations } from 'matrix-js-sdk/src/models/relations';
 
 import { _t } from '../../../languageHandler';
 import * as sdk from '../../../index';
@@ -35,13 +36,17 @@ import Resend from "../../../Resend";
 import { MatrixClientPeg } from "../../../MatrixClientPeg";
 import { MediaEventHelper } from "../../../utils/MediaEventHelper";
 import DownloadActionButton from "./DownloadActionButton";
+import MessageContextMenu from "../context_menus/MessageContextMenu";
+import classNames from 'classnames';
+
 import SettingsStore from '../../../settings/SettingsStore';
 import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
 import ReplyThread from '../elements/ReplyThread';
 
 interface IOptionsButtonProps {
     mxEvent: MatrixEvent;
-    getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
+    // TODO: Types
+    getTile: () => any | null;
     getReplyThread: () => ReplyThread;
     permalinkCreator: RoomPermalinkCreator;
     onFocusChange: (menuDisplayed: boolean) => void;
@@ -57,8 +62,6 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
 
         let contextMenu;
         if (menuDisplayed) {
-            const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
-
             const tile = getTile && getTile();
             const replyThread = getReplyThread && getReplyThread();
 
@@ -90,7 +93,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> =
 
 interface IReactButtonProps {
     mxEvent: MatrixEvent;
-    reactions: any; // TODO: types
+    reactions: Relations;
     onFocusChange: (menuDisplayed: boolean) => void;
 }
 
@@ -125,20 +128,32 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC
     </React.Fragment>;
 };
 
+export enum ActionBarRenderingContext {
+    Room,
+    Thread
+}
+
 interface IMessageActionBarProps {
     mxEvent: MatrixEvent;
-    // The Relations model from the JS SDK for reactions to `mxEvent`
-    reactions?: any;  // TODO: types
+    reactions?: Relations;
+    // TODO: Types
+    getTile: () => any | null;
+    getReplyThread: () => ReplyThread | undefined;
     permalinkCreator?: RoomPermalinkCreator;
-    getTile: () => any; // TODO: FIXME, haven't figured out what the return type is here
-    getReplyThread?: () => ReplyThread;
     onFocusChange?: (menuDisplayed: boolean) => void;
+    toggleThreadExpanded: () => void;
+    renderingContext?: ActionBarRenderingContext;
+    isQuoteExpanded?: boolean;
 }
 
 @replaceableComponent("views.messages.MessageActionBar")
 export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
     public static contextType = RoomContext;
 
+    public static defaultProps = {
+        renderingContext: ActionBarRenderingContext.Room,
+    };
+
     public componentDidMount(): void {
         if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
             this.props.mxEvent.on("Event.status", this.onSent);
@@ -283,7 +298,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
                 // Like the resend button, the react and reply buttons need to appear before the edit.
                 // The only catch is we do the reply button first so that we can make sure the react
                 // button is the very first button without having to do length checks for `splice()`.
-                if (this.context.canReply) {
+                if (this.context.canReply && this.props.renderingContext === ActionBarRenderingContext.Room) {
                     toolbarOpts.splice(0, 0, <>
                         <RovingAccessibleTooltipButton
                             className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
@@ -324,6 +339,20 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
                 toolbarOpts.push(cancelSendingButton);
             }
 
+            if (this.props.isQuoteExpanded !== undefined && ReplyThread.hasThreadReply(this.props.mxEvent)) {
+                const expandClassName = classNames({
+                    'mx_MessageActionBar_maskButton': true,
+                    'mx_MessageActionBar_expandMessageButton': !this.props.isQuoteExpanded,
+                    'mx_MessageActionBar_collapseMessageButton': this.props.isQuoteExpanded,
+                });
+                toolbarOpts.push(<RovingAccessibleTooltipButton
+                    className={expandClassName}
+                    title={this.props.isQuoteExpanded ? _t("Collapse quotes │ ⇧+click") : _t("Expand quotes │ ⇧+click")}
+                    onClick={this.props.toggleThreadExpanded}
+                    key="expand"
+                />);
+            }
+
             // The menu button should be last, so dump it there.
             toolbarOpts.push(<OptionsButton
                 mxEvent={this.props.mxEvent}
diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx
index 83fe7f5a3d..63ff39721d 100644
--- a/src/components/views/messages/TextualBody.tsx
+++ b/src/components/views/messages/TextualBody.tsx
@@ -138,6 +138,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
         // If it's less than 30% we don't add the expansion button.
         // We also round the number as it sometimes can be 29.99...
         const percentageOfViewport = Math.round(pre.offsetHeight / UIStore.instance.windowHeight * 100);
+        // TODO: additionally show the button if it's an expanded quoted message
         if (percentageOfViewport < 30) return;
 
         const button = document.createElement("span");
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 4a62d6711e..7afa29624a 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -15,7 +15,6 @@ limitations under the License.
 */
 
 import React from 'react';
-import classNames from 'classnames';
 import { lexicographicCompare } from 'matrix-js-sdk/src/utils';
 import { Room } from 'matrix-js-sdk/src/models/room';
 
@@ -35,16 +34,6 @@ interface IProps {
     room: Room;
     userId: string;
     showApps: boolean; // Render apps
-
-    // maxHeight attribute for the aux panel and the video
-    // therein
-    maxHeight: number;
-
-    // a callback which is called when the content of the aux panel changes
-    // content in a way that is likely to make it change size.
-    onResize: () => void;
-    fullHeight: boolean;
-
     resizeNotifier: ResizeNotifier;
 }
 
@@ -92,13 +81,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
         return objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState);
     }
 
-    componentDidUpdate(prevProps, prevState) {
-        // most changes are likely to cause a resize
-        if (this.props.onResize) {
-            this.props.onResize();
-        }
-    }
-
     private rateLimitedUpdate = throttle(() => {
         this.setState({ counters: this.computeCounters() });
     }, 500, { leading: true, trailing: true });
@@ -138,7 +120,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
         const callView = (
             <CallViewForRoom
                 roomId={this.props.room.roomId}
-                maxVideoHeight={this.props.maxHeight}
                 resizeNotifier={this.props.resizeNotifier}
             />
         );
@@ -148,7 +129,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
             appsDrawer = <AppsDrawer
                 room={this.props.room}
                 userId={this.props.userId}
-                maxHeight={this.props.maxHeight}
                 showApps={this.props.showApps}
                 resizeNotifier={this.props.resizeNotifier}
             />;
@@ -204,21 +184,12 @@ export default class AuxPanel extends React.Component<IProps, IState> {
             }
         }
 
-        const classes = classNames({
-            "mx_RoomView_auxPanel": true,
-            "mx_RoomView_auxPanel_fullHeight": this.props.fullHeight,
-        });
-        const style: React.CSSProperties = {};
-        if (!this.props.fullHeight) {
-            style.maxHeight = this.props.maxHeight;
-        }
-
         return (
-            <AutoHideScrollbar className={classes} style={style}>
+            <AutoHideScrollbar className="mx_RoomView_auxPanel">
                 { stateViews }
+                { this.props.children }
                 { appsDrawer }
                 { callView }
-                { this.props.children }
             </AutoHideScrollbar>
         );
     }
diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx
index f2f80b7670..33273f1f95 100644
--- a/src/components/views/rooms/EditMessageComposer.tsx
+++ b/src/components/views/rooms/EditMessageComposer.tsx
@@ -42,6 +42,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
 import QuestionDialog from "../dialogs/QuestionDialog";
 import { ActionPayload } from "../../../dispatcher/payloads";
 import AccessibleButton from '../elements/AccessibleButton';
+import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
 import SettingsStore from "../../../settings/SettingsStore";
 
 import { logger } from "matrix-js-sdk/src/logger";
@@ -331,6 +332,14 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
 
         let shouldSend = true;
 
+        if (newContent?.body === '') {
+            this.cancelPreviousPendingEdit();
+            createRedactEventDialog({
+                mxEvent: editedEvent,
+            });
+            return;
+        }
+
         // If content is modified then send an updated event into the room
         if (this.isContentModified(newContent)) {
             const roomId = editedEvent.getRoomId();
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index d1ac06b199..27cf7d761f 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -53,7 +53,7 @@ import SenderProfile from '../messages/SenderProfile';
 import MessageTimestamp from '../messages/MessageTimestamp';
 import TooltipButton from '../elements/TooltipButton';
 import ReadReceiptMarker from "./ReadReceiptMarker";
-import MessageActionBar from "../messages/MessageActionBar";
+import MessageActionBar, { ActionBarRenderingContext } from "../messages/MessageActionBar";
 import ReactionsRow from '../messages/ReactionsRow';
 import { getEventDisplayInfo } from '../../../utils/EventUtils';
 import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
@@ -192,6 +192,7 @@ export enum TileShape {
     Notif = "notif",
     FileGrid = "file_grid",
     Pinned = "pinned",
+    Thread = "thread",
 }
 
 interface IProps {
@@ -322,7 +323,7 @@ interface IState {
     reactions: Relations;
 
     hover: boolean;
-
+    isQuoteExpanded?: boolean;
     thread?: Thread;
 }
 
@@ -330,7 +331,8 @@ interface IState {
 export default class EventTile extends React.Component<IProps, IState> {
     private suppressReadReceiptAnimation: boolean;
     private isListeningForReceipts: boolean;
-    private tile = React.createRef();
+    // TODO: Types
+    private tile = React.createRef<unknown>();
     private replyThread = React.createRef<ReplyThread>();
 
     public readonly ref = createRef<HTMLElement>();
@@ -888,8 +890,8 @@ export default class EventTile extends React.Component<IProps, IState> {
             actionBarFocused: focused,
         });
     };
-
-    getTile = () => this.tile.current;
+    // TODO: Types
+    getTile: () => any | null = () => this.tile.current;
 
     getReplyThread = () => this.replyThread.current;
 
@@ -914,6 +916,11 @@ export default class EventTile extends React.Component<IProps, IState> {
         });
     };
 
+    private setQuoteExpanded = (expanded: boolean) => {
+        this.setState({
+            isQuoteExpanded: expanded,
+        });
+    };
     render() {
         const msgtype = this.props.mxEvent.getContent().msgtype;
         const eventType = this.props.mxEvent.getType() as EventType;
@@ -923,6 +930,7 @@ export default class EventTile extends React.Component<IProps, IState> {
             isInfoMessage,
             isLeftAlignedBubbleMessage,
         } = getEventDisplayInfo(this.props.mxEvent);
+        const { isQuoteExpanded } = this.state;
 
         // This shouldn't happen: the caller should check we support this type
         // before trying to instantiate us
@@ -935,6 +943,7 @@ export default class EventTile extends React.Component<IProps, IState> {
                 </div>
             </div>;
         }
+
         const EventTileType = sdk.getComponent(tileHandler);
 
         const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
@@ -1047,6 +1056,9 @@ export default class EventTile extends React.Component<IProps, IState> {
             }
         }
 
+        const renderingContext = this.props.tileShape === TileShape.Thread
+            ? ActionBarRenderingContext.Thread
+            : ActionBarRenderingContext.Room;
         const actionBar = !isEditing ? <MessageActionBar
             mxEvent={this.props.mxEvent}
             reactions={this.state.reactions}
@@ -1054,6 +1066,9 @@ export default class EventTile extends React.Component<IProps, IState> {
             getTile={this.getTile}
             getReplyThread={this.getReplyThread}
             onFocusChange={this.onActionBarFocusChange}
+            renderingContext={renderingContext}
+            isQuoteExpanded={isQuoteExpanded}
+            toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
         /> : undefined;
 
         const showTimestamp = this.props.mxEvent.getTs()
@@ -1160,6 +1175,40 @@ export default class EventTile extends React.Component<IProps, IState> {
                     </div>,
                 ]);
             }
+            case TileShape.Thread: {
+                const room = this.context.getRoom(this.props.mxEvent.getRoomId());
+                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}
+                            tileShape={this.props.tileShape}
+                        />
+                        { actionBar }
+                    </div>,
+                ]);
+            }
             case TileShape.FileGrid: {
                 return React.createElement(this.props.as || "li", {
                     "className": classes,
@@ -1192,20 +1241,18 @@ export default class EventTile extends React.Component<IProps, IState> {
             }
 
             default: {
-                let thread;
-                // When the "showHiddenEventsInTimeline" lab is enabled,
-                // avoid showing replies for hidden events (events without tiles)
-                if (haveTileForEvent(this.props.mxEvent)) {
-                    thread = ReplyThread.makeThread(
-                        this.props.mxEvent,
-                        this.props.onHeightChanged,
-                        this.props.permalinkCreator,
-                        this.replyThread,
-                        this.props.layout,
-                        this.props.alwaysShowTimestamps || this.state.hover,
-                    );
-                }
-
+                const thread = haveTileForEvent(this.props.mxEvent) &&
+                    ReplyThread.hasThreadReply(this.props.mxEvent) ? (
+                        <ReplyThread
+                            parentEv={this.props.mxEvent}
+                            onHeightChanged={this.props.onHeightChanged}
+                            ref={this.replyThread}
+                            permalinkCreator={this.props.permalinkCreator}
+                            layout={this.props.layout}
+                            alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
+                            isQuoteExpanded={isQuoteExpanded}
+                            setQuoteExpanded={this.setQuoteExpanded}
+                        />) : null;
                 const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
 
                 // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx
index c9842bdd33..eed13aff0f 100644
--- a/src/components/views/rooms/LinkPreviewGroup.tsx
+++ b/src/components/views/rooms/LinkPreviewGroup.tsx
@@ -16,7 +16,7 @@ limitations under the License.
 
 import React, { useContext, useEffect } from "react";
 import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
+import { IPreviewUrlResponse, MatrixClient } from "matrix-js-sdk/src/client";
 
 import { useStateToggle } from "../../../hooks/useStateToggle";
 import LinkPreviewWidget from "./LinkPreviewWidget";
@@ -40,13 +40,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
 
     const ts = mxEvent.getTs();
     const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
-        return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
-            try {
-                return [link, await cli.getUrlPreview(link, ts)];
-            } catch (error) {
-                console.error("Failed to get URL preview: " + error);
-            }
-        })).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
+        return fetchPreviews(cli, links, ts);
     }, [links, ts], []);
 
     useEffect(() => {
@@ -89,4 +83,18 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
     </div>;
 };
 
+const fetchPreviews = (cli: MatrixClient, links: string[], ts: number):
+        Promise<[string, IPreviewUrlResponse][]> => {
+    return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
+        try {
+            const preview = await cli.getUrlPreview(link, ts);
+            if (preview && Object.keys(preview).length > 0) {
+                return [link, preview];
+            }
+        } catch (error) {
+            console.error("Failed to get URL preview: " + error);
+        }
+    })).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
+};
+
 export default LinkPreviewGroup;
diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx
index cf7d1ce945..01a9e2f18b 100644
--- a/src/components/views/rooms/ReplyTile.tsx
+++ b/src/components/views/rooms/ReplyTile.tsx
@@ -35,6 +35,7 @@ interface IProps {
     highlights?: string[];
     highlightLink?: string;
     onHeightChanged?(): void;
+    toggleExpandedQuote?: () => void;
 }
 
 @replaceableComponent("views.rooms.ReplyTile")
@@ -82,12 +83,17 @@ export default class ReplyTile extends React.PureComponent<IProps> {
             // This allows the permalink to be opened in a new tab/window or copied as
             // matrix.to, but also for it to enable routing within Riot when clicked.
             e.preventDefault();
-            dis.dispatch({
-                action: 'view_room',
-                event_id: this.props.mxEvent.getId(),
-                highlighted: true,
-                room_id: this.props.mxEvent.getRoomId(),
-            });
+            // Expand thread on shift key
+            if (this.props.toggleExpandedQuote && e.shiftKey) {
+                this.props.toggleExpandedQuote();
+            } else {
+                dis.dispatch({
+                    action: 'view_room',
+                    event_id: this.props.mxEvent.getId(),
+                    highlighted: true,
+                    room_id: this.props.mxEvent.getRoomId(),
+                });
+            }
         }
     };
 
diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx
index a5aa3e7734..b0a6f17095 100644
--- a/src/components/views/voip/CallViewForRoom.tsx
+++ b/src/components/views/voip/CallViewForRoom.tsx
@@ -27,9 +27,6 @@ interface IProps {
     // What room we should display the call for
     roomId: string;
 
-    // maxHeight style attribute for the video panel
-    maxVideoHeight?: number;
-
     resizeNotifier: ResizeNotifier;
 }
 
@@ -99,14 +96,12 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
 
     public render() {
         if (!this.state.call) return null;
-        // We subtract 8 as it the margin-bottom of the mx_CallViewForRoom_ResizeWrapper
-        const maxHeight = this.props.maxVideoHeight - 8;
 
         return (
             <div className="mx_CallViewForRoom">
                 <Resizable
                     minHeight={380}
-                    maxHeight={maxHeight}
+                    maxHeight="80vh"
                     enable={{
                         top: false,
                         right: false,
diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts
index 38a73cc945..9822046a0d 100644
--- a/src/editor/serialize.ts
+++ b/src/editor/serialize.ts
@@ -185,7 +185,7 @@ export function startsWith(model: EditorModel, prefix: string, caseSensitive = t
     const firstPart = model.parts[0];
     // part type will be "plain" while editing,
     // and "command" while composing a message.
-    let text = firstPart && firstPart.text;
+    let text = firstPart?.text || '';
     if (!caseSensitive) {
         prefix = prefix.toLowerCase();
         text = text.toLowerCase();
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index d23be9f413..83f1e1988d 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1955,6 +1955,8 @@
     "Edit": "Edit",
     "Reply": "Reply",
     "Thread": "Thread",
+    "Collapse quotes │ ⇧+click": "Collapse quotes │ ⇧+click",
+    "Expand quotes │ ⇧+click": "Expand quotes │ ⇧+click",
     "Message Actions": "Message Actions",
     "Download %(text)s": "Download %(text)s",
     "Error decrypting attachment": "Error decrypting attachment",
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index f28d279d00..d440c33c83 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -818,7 +818,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
     }
 
     protected async onAction(payload: ActionPayload) {
-        if (!spacesEnabled) return;
+        if (!spacesEnabled || !this.matrixClient) return;
         switch (payload.action) {
             case "view_room": {
                 // Don't auto-switch rooms when reacting to a context-switch
diff --git a/yarn.lock b/yarn.lock
index 622d96cc0a..39c50464d5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5807,8 +5807,8 @@ mathml-tag-names@^2.1.3:
   integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
 
 "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
-  version "12.5.0"
-  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f84905b00398072b592addfb1dae64c8f3a07fa2"
+  version "13.0.0"
+  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2515d07c8fc3bf5e1afc8352e3e330cca30dde85"
   dependencies:
     "@babel/runtime" "^7.12.5"
     another-json "^0.2.0"