diff --git a/.eslintrc.js b/.eslintrc.js index bc2a142c2d..99695b7a03 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,6 +22,8 @@ module.exports = { "files": ["src/**/*.{ts,tsx}"], "extends": ["matrix-org/ts"], "rules": { + // We're okay being explicit at the moment + "@typescript-eslint/no-empty-interface": "off", // We disable this while we're transitioning "@typescript-eslint/no-explicit-any": "off", // We'd rather not do this but we do diff --git a/.gitignore b/.gitignore index 33e8bfc7ac..e1dd7726e1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ package-lock.json /src/component-index.js .DS_Store +*.tmp diff --git a/CHANGELOG.md b/CHANGELOG.md index 151888a17e..c839fc2b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,678 @@ +Changes in [3.17.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.17.0) (2021-03-29) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.17.0-rc.1...v3.17.0) + + * Upgrade to JS SDK 9.10.0 + * [Release] Tweak cross-signing copy + [\#5808](https://github.com/matrix-org/matrix-react-sdk/pull/5808) + * [Release] Fix crash on login when using social login + [\#5809](https://github.com/matrix-org/matrix-react-sdk/pull/5809) + * [Release] Fix edge case with redaction grouper messing up continuations + [\#5799](https://github.com/matrix-org/matrix-react-sdk/pull/5799) + +Changes in [3.17.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.17.0-rc.1) (2021-03-25) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0...v3.17.0-rc.1) + + * Upgrade to JS SDK 9.10.0-rc.1 + * Translations update from Weblate + [\#5788](https://github.com/matrix-org/matrix-react-sdk/pull/5788) + * Track next event [tile] over group boundaries + [\#5784](https://github.com/matrix-org/matrix-react-sdk/pull/5784) + * Fixing the minor UI issues in the email discovery + [\#5780](https://github.com/matrix-org/matrix-react-sdk/pull/5780) + * Don't overwrite callback with undefined if no customization provided + [\#5783](https://github.com/matrix-org/matrix-react-sdk/pull/5783) + * Fix redaction event list summaries breaking sender profiles + [\#5781](https://github.com/matrix-org/matrix-react-sdk/pull/5781) + * Fix CIDER formatting buttons on Safari + [\#5782](https://github.com/matrix-org/matrix-react-sdk/pull/5782) + * Improve discovery of rooms in a space + [\#5776](https://github.com/matrix-org/matrix-react-sdk/pull/5776) + * Spaces improve creation journeys + [\#5777](https://github.com/matrix-org/matrix-react-sdk/pull/5777) + * Make buttons in verify dialog respect the system font + [\#5778](https://github.com/matrix-org/matrix-react-sdk/pull/5778) + * Collapse redactions into an event list summary + [\#5728](https://github.com/matrix-org/matrix-react-sdk/pull/5728) + * Added invite option to room's context menu + [\#5648](https://github.com/matrix-org/matrix-react-sdk/pull/5648) + * Add an optional config option to make the welcome page the login page + [\#5658](https://github.com/matrix-org/matrix-react-sdk/pull/5658) + * Fix username showing instead of display name in Jitsi widgets + [\#5770](https://github.com/matrix-org/matrix-react-sdk/pull/5770) + * Convert a bunch more js-sdk imports to absolute paths + [\#5774](https://github.com/matrix-org/matrix-react-sdk/pull/5774) + * Remove forgotten rooms from the room list once forgotten + [\#5775](https://github.com/matrix-org/matrix-react-sdk/pull/5775) + * Log error when failing to list usermedia devices + [\#5771](https://github.com/matrix-org/matrix-react-sdk/pull/5771) + * Fix weird timeline jumps + [\#5772](https://github.com/matrix-org/matrix-react-sdk/pull/5772) + * Replace type declaration in Registration.tsx + [\#5773](https://github.com/matrix-org/matrix-react-sdk/pull/5773) + * Add possibility to delay rageshake persistence in app startup + [\#5767](https://github.com/matrix-org/matrix-react-sdk/pull/5767) + * Fix left panel resizing and lower min-width improving flexibility + [\#5764](https://github.com/matrix-org/matrix-react-sdk/pull/5764) + * Work around more cases where a rageshake server might not be present + [\#5766](https://github.com/matrix-org/matrix-react-sdk/pull/5766) + * Iterate space panel visually and functionally + [\#5761](https://github.com/matrix-org/matrix-react-sdk/pull/5761) + * Make some dispatches async + [\#5765](https://github.com/matrix-org/matrix-react-sdk/pull/5765) + * fix: make room directory correct when using a homeserver with explicit port + [\#5762](https://github.com/matrix-org/matrix-react-sdk/pull/5762) + * Hangup all calls on logout + [\#5756](https://github.com/matrix-org/matrix-react-sdk/pull/5756) + * Remove now-unused assets and CSS from CompleteSecurity step + [\#5757](https://github.com/matrix-org/matrix-react-sdk/pull/5757) + * Add details and summary to allowed HTML tags + [\#5760](https://github.com/matrix-org/matrix-react-sdk/pull/5760) + * Support a media handling customisation endpoint + [\#5714](https://github.com/matrix-org/matrix-react-sdk/pull/5714) + * Edit button on View Source dialog that takes you to devtools -> + SendCustomEvent + [\#5718](https://github.com/matrix-org/matrix-react-sdk/pull/5718) + * Show room alias in plain/formatted body + [\#5748](https://github.com/matrix-org/matrix-react-sdk/pull/5748) + * Allow pills on the beginning of a part string + [\#5754](https://github.com/matrix-org/matrix-react-sdk/pull/5754) + * [SK-3] Decorate easy components with replaceableComponent + [\#5734](https://github.com/matrix-org/matrix-react-sdk/pull/5734) + * Use fsync in reskindex to ensure file is written to disk + [\#5753](https://github.com/matrix-org/matrix-react-sdk/pull/5753) + * Remove unused common CSS classes + [\#5752](https://github.com/matrix-org/matrix-react-sdk/pull/5752) + * Rebuild space previews with new designs + [\#5751](https://github.com/matrix-org/matrix-react-sdk/pull/5751) + * Rework cross-signing login flow + [\#5727](https://github.com/matrix-org/matrix-react-sdk/pull/5727) + * Change read receipt drift to be non-fractional + [\#5745](https://github.com/matrix-org/matrix-react-sdk/pull/5745) + +Changes in [3.16.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0) (2021-03-15) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.2...v3.16.0) + + * Upgrade to JS SDK 9.9.0 + * [Release] Change read receipt drift to be non-fractional + [\#5746](https://github.com/matrix-org/matrix-react-sdk/pull/5746) + * [Release] Properly gate SpaceRoomView behind labs + [\#5750](https://github.com/matrix-org/matrix-react-sdk/pull/5750) + +Changes in [3.16.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.2) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.1...v3.16.0-rc.2) + + * Fixed incorrect build output in rc.1 + +Changes in [3.16.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.1) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0...v3.16.0-rc.1) + + * Upgrade to JS SDK 9.9.0-rc.1 + * Translations update from Weblate + [\#5743](https://github.com/matrix-org/matrix-react-sdk/pull/5743) + * Document behaviour of showReadReceipts=false for sent receipts + [\#5739](https://github.com/matrix-org/matrix-react-sdk/pull/5739) + * Tweak sent marker code style + [\#5741](https://github.com/matrix-org/matrix-react-sdk/pull/5741) + * Fix sent markers disappearing for edits/reactions + [\#5737](https://github.com/matrix-org/matrix-react-sdk/pull/5737) + * Ignore to-device decryption in the room list store + [\#5740](https://github.com/matrix-org/matrix-react-sdk/pull/5740) + * Spaces suggested rooms support + [\#5736](https://github.com/matrix-org/matrix-react-sdk/pull/5736) + * Add tooltips to sent/sending receipts + [\#5738](https://github.com/matrix-org/matrix-react-sdk/pull/5738) + * Remove a bunch of useless 'use strict' definitions + [\#5735](https://github.com/matrix-org/matrix-react-sdk/pull/5735) + * [SK-1] Fix types for replaceableComponent + [\#5732](https://github.com/matrix-org/matrix-react-sdk/pull/5732) + * [SK-2] Make debugging skinning problems easier + [\#5733](https://github.com/matrix-org/matrix-react-sdk/pull/5733) + * Support sending invite reasons with /invite command + [\#5695](https://github.com/matrix-org/matrix-react-sdk/pull/5695) + * Fix clicking on the avatar for opening member info requires pixel-perfect + accuracy + [\#5717](https://github.com/matrix-org/matrix-react-sdk/pull/5717) + * Display decrypted and encrypted event source on the same dialog + [\#5713](https://github.com/matrix-org/matrix-react-sdk/pull/5713) + * Fix units of TURN server expiry time + [\#5730](https://github.com/matrix-org/matrix-react-sdk/pull/5730) + * Display room name in pills instead of address + [\#5624](https://github.com/matrix-org/matrix-react-sdk/pull/5624) + * Refresh UI for file uploads + [\#5723](https://github.com/matrix-org/matrix-react-sdk/pull/5723) + * UI refresh for uploaded files + [\#5719](https://github.com/matrix-org/matrix-react-sdk/pull/5719) + * Improve message sending states to match new designs + [\#5699](https://github.com/matrix-org/matrix-react-sdk/pull/5699) + * Add clipboard write permission for widgets + [\#5725](https://github.com/matrix-org/matrix-react-sdk/pull/5725) + * Fix widget resizing + [\#5722](https://github.com/matrix-org/matrix-react-sdk/pull/5722) + * Option for audio streaming + [\#5707](https://github.com/matrix-org/matrix-react-sdk/pull/5707) + * Show a specific error for hs_disabled + [\#5576](https://github.com/matrix-org/matrix-react-sdk/pull/5576) + * Add Edge to the targets list + [\#5721](https://github.com/matrix-org/matrix-react-sdk/pull/5721) + * File drop UI fixes and improvements + [\#5505](https://github.com/matrix-org/matrix-react-sdk/pull/5505) + * Fix Bottom border of state counters is white on the dark theme + [\#5715](https://github.com/matrix-org/matrix-react-sdk/pull/5715) + * Trim spurious whitespace of nicknames + [\#5332](https://github.com/matrix-org/matrix-react-sdk/pull/5332) + * Ensure HostSignupDialog border colour matches light theme + [\#5716](https://github.com/matrix-org/matrix-react-sdk/pull/5716) + * Don't place another call if there's already one ongoing + [\#5712](https://github.com/matrix-org/matrix-react-sdk/pull/5712) + * Space room hierarchies + [\#5706](https://github.com/matrix-org/matrix-react-sdk/pull/5706) + * Iterate Space view and right panel + [\#5705](https://github.com/matrix-org/matrix-react-sdk/pull/5705) + * Add a scroll to bottom on message sent setting + [\#5692](https://github.com/matrix-org/matrix-react-sdk/pull/5692) + * Add .tmp files to gitignore + [\#5708](https://github.com/matrix-org/matrix-react-sdk/pull/5708) + * Initial Space Room View and Creation UX + [\#5704](https://github.com/matrix-org/matrix-react-sdk/pull/5704) + * Add multi language spell check + [\#5452](https://github.com/matrix-org/matrix-react-sdk/pull/5452) + * Fix tetris effect (holes) in read receipts + [\#5697](https://github.com/matrix-org/matrix-react-sdk/pull/5697) + * Fixed edit for markdown images + [\#5703](https://github.com/matrix-org/matrix-react-sdk/pull/5703) + * Iterate Space Panel + [\#5702](https://github.com/matrix-org/matrix-react-sdk/pull/5702) + * Fix read receipts for compact layout + [\#5700](https://github.com/matrix-org/matrix-react-sdk/pull/5700) + * Space Store and Space Panel for Room List filtering + [\#5689](https://github.com/matrix-org/matrix-react-sdk/pull/5689) + * Log when turn creds expire + [\#5691](https://github.com/matrix-org/matrix-react-sdk/pull/5691) + * Null check for maxHeight in call view + [\#5690](https://github.com/matrix-org/matrix-react-sdk/pull/5690) + * Autocomplete invited users + [\#5687](https://github.com/matrix-org/matrix-react-sdk/pull/5687) + * Add send message button + [\#5535](https://github.com/matrix-org/matrix-react-sdk/pull/5535) + * Move call buttons to the room header + [\#5693](https://github.com/matrix-org/matrix-react-sdk/pull/5693) + * Use the default SSSS key if the default is set + [\#5638](https://github.com/matrix-org/matrix-react-sdk/pull/5638) + * Initial Spaces feature flag + [\#5668](https://github.com/matrix-org/matrix-react-sdk/pull/5668) + * Clean up code edge cases and add helpers + [\#5667](https://github.com/matrix-org/matrix-react-sdk/pull/5667) + * Clean up widgets when leaving the room + [\#5684](https://github.com/matrix-org/matrix-react-sdk/pull/5684) + * Fix read receipts? + [\#5567](https://github.com/matrix-org/matrix-react-sdk/pull/5567) + * Fix MAU usage alerts + [\#5678](https://github.com/matrix-org/matrix-react-sdk/pull/5678) + +Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0) + +## Security notice + +matrix-react-sdk 3.15.0 fixes a low severity issue (CVE-2021-21320) where the +user content sandbox can be abused to trick users into opening unexpected +documents. The content is opened with a `blob` origin that cannot access Matrix +user data, so messages and secrets are not at risk. Thanks to @keerok for +responsibly disclosing this via Matrix's Security Disclosure Policy. + +## All changes + + * Upgrade to JS SDK 9.8.0 + +Changes in [3.15.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0-rc.1) (2021-02-24) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0...v3.15.0-rc.1) + + * Upgrade to JS SDK 9.8.0-rc.1 + * Translations update from Weblate + [\#5683](https://github.com/matrix-org/matrix-react-sdk/pull/5683) + * Fix object diffing when objects have different keys + [\#5681](https://github.com/matrix-org/matrix-react-sdk/pull/5681) + * Add if it's missing + [\#5673](https://github.com/matrix-org/matrix-react-sdk/pull/5673) + * Add email only if the verification is complete + [\#5629](https://github.com/matrix-org/matrix-react-sdk/pull/5629) + * Fix portrait videocalls + [\#5676](https://github.com/matrix-org/matrix-react-sdk/pull/5676) + * Tweak code block icon positions + [\#5643](https://github.com/matrix-org/matrix-react-sdk/pull/5643) + * Revert "Improve URL preview formatting and image upload thumbnail size" + [\#5677](https://github.com/matrix-org/matrix-react-sdk/pull/5677) + * Fix context menu leaving visible area + [\#5644](https://github.com/matrix-org/matrix-react-sdk/pull/5644) + * Jitsi conferences names, take 3 + [\#5675](https://github.com/matrix-org/matrix-react-sdk/pull/5675) + * Update isUserOnDarkTheme to take use_system_theme in account + [\#5670](https://github.com/matrix-org/matrix-react-sdk/pull/5670) + * Discard some dead code + [\#5665](https://github.com/matrix-org/matrix-react-sdk/pull/5665) + * Add developer tool to explore and edit settings + [\#5664](https://github.com/matrix-org/matrix-react-sdk/pull/5664) + * Use and create new room helpers + [\#5663](https://github.com/matrix-org/matrix-react-sdk/pull/5663) + * Clear message previews when the maximum limit is reached for history + [\#5661](https://github.com/matrix-org/matrix-react-sdk/pull/5661) + * VoIP virtual rooms, mk II + [\#5639](https://github.com/matrix-org/matrix-react-sdk/pull/5639) + * Disable chat effects when reduced motion preferred + [\#5660](https://github.com/matrix-org/matrix-react-sdk/pull/5660) + * Improve URL preview formatting and image upload thumbnail size + [\#5637](https://github.com/matrix-org/matrix-react-sdk/pull/5637) + * Fix border radius when the panel is collapsed + [\#5641](https://github.com/matrix-org/matrix-react-sdk/pull/5641) + * Use a more generic layout setting - useIRCLayout → layout + [\#5571](https://github.com/matrix-org/matrix-react-sdk/pull/5571) + * Remove redundant lockOrigin parameter from usercontent + [\#5657](https://github.com/matrix-org/matrix-react-sdk/pull/5657) + * Set ICE candidate pool size option + [\#5655](https://github.com/matrix-org/matrix-react-sdk/pull/5655) + * Prepare to encrypt when a call arrives + [\#5654](https://github.com/matrix-org/matrix-react-sdk/pull/5654) + * Use config for host signup branding + [\#5650](https://github.com/matrix-org/matrix-react-sdk/pull/5650) + * Use randomly generated conference names for Jitsi + [\#5649](https://github.com/matrix-org/matrix-react-sdk/pull/5649) + * Modified regex to account for an immediate new line after slash commands + [\#5647](https://github.com/matrix-org/matrix-react-sdk/pull/5647) + * Fix codeblock scrollbar color for non-Firefox + [\#5642](https://github.com/matrix-org/matrix-react-sdk/pull/5642) + * Fix codeblock scrollbar colors + [\#5630](https://github.com/matrix-org/matrix-react-sdk/pull/5630) + * Added loading and disabled the button while searching for server + [\#5634](https://github.com/matrix-org/matrix-react-sdk/pull/5634) + +Changes in [3.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0) (2021-02-16) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0-rc.1...v3.14.0) + + * Upgrade to JS SDK 9.7.0 + * [Release] Use config for host signup branding + [\#5651](https://github.com/matrix-org/matrix-react-sdk/pull/5651) + +Changes in [3.14.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0-rc.1) (2021-02-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.1...v3.14.0-rc.1) + + * Upgrade to JS SDK 9.7.0-rc.1 + * Translations update from Weblate + [\#5636](https://github.com/matrix-org/matrix-react-sdk/pull/5636) + * Add host signup modal with iframe + [\#5450](https://github.com/matrix-org/matrix-react-sdk/pull/5450) + * Fix duplication of codeblock elements + [\#5633](https://github.com/matrix-org/matrix-react-sdk/pull/5633) + * Handle undefined call stats + [\#5632](https://github.com/matrix-org/matrix-react-sdk/pull/5632) + * Avoid delayed displaying of sources in source picker + [\#5631](https://github.com/matrix-org/matrix-react-sdk/pull/5631) + * Give breadcrumbs toolbar an accessibility label. + [\#5628](https://github.com/matrix-org/matrix-react-sdk/pull/5628) + * Fix the %s in logs + [\#5627](https://github.com/matrix-org/matrix-react-sdk/pull/5627) + * Fix jumpy notifications settings UI + [\#5625](https://github.com/matrix-org/matrix-react-sdk/pull/5625) + * Improve displaying of code blocks + [\#5559](https://github.com/matrix-org/matrix-react-sdk/pull/5559) + * Fix desktop Matrix screen sharing and add a screen/window picker + [\#5525](https://github.com/matrix-org/matrix-react-sdk/pull/5525) + * Call "MatrixClientPeg.get()" only once in method "findOverrideMuteRule" + [\#5498](https://github.com/matrix-org/matrix-react-sdk/pull/5498) + * Close current modal when session is logged out + [\#5616](https://github.com/matrix-org/matrix-react-sdk/pull/5616) + * Switch room explorer list to CSS grid + [\#5551](https://github.com/matrix-org/matrix-react-sdk/pull/5551) + * Improve SSO login start screen and 3pid invite handling somewhat + [\#5622](https://github.com/matrix-org/matrix-react-sdk/pull/5622) + * Don't jump to bottom on reaction + [\#5621](https://github.com/matrix-org/matrix-react-sdk/pull/5621) + * Fix several profile settings oddities + [\#5620](https://github.com/matrix-org/matrix-react-sdk/pull/5620) + * Add option to hide the stickers button in the composer + [\#5530](https://github.com/matrix-org/matrix-react-sdk/pull/5530) + * Fix confusing right panel button behaviour + [\#5598](https://github.com/matrix-org/matrix-react-sdk/pull/5598) + * Fix jumping timestamp if hovering a message with e2e indicator bar + [\#5601](https://github.com/matrix-org/matrix-react-sdk/pull/5601) + * Fix avatar and trash alignment + [\#5614](https://github.com/matrix-org/matrix-react-sdk/pull/5614) + * Fix z-index of stickerpicker + [\#5617](https://github.com/matrix-org/matrix-react-sdk/pull/5617) + * Fix permalink via parsing for rooms + [\#5615](https://github.com/matrix-org/matrix-react-sdk/pull/5615) + * Fix "Terms and Conditions" checkbox alignment + [\#5613](https://github.com/matrix-org/matrix-react-sdk/pull/5613) + * Fix flair height after accent changes + [\#5611](https://github.com/matrix-org/matrix-react-sdk/pull/5611) + * Iterate Social Logins work around edge cases and branding + [\#5609](https://github.com/matrix-org/matrix-react-sdk/pull/5609) + * Lock widget room ID when added + [\#5607](https://github.com/matrix-org/matrix-react-sdk/pull/5607) + * Better errors for SSO failures + [\#5605](https://github.com/matrix-org/matrix-react-sdk/pull/5605) + * Increase language search bar width + [\#5549](https://github.com/matrix-org/matrix-react-sdk/pull/5549) + * Scroll to bottom on message_sent + [\#5565](https://github.com/matrix-org/matrix-react-sdk/pull/5565) + * Fix new rooms being titled 'Empty Room' + [\#5587](https://github.com/matrix-org/matrix-react-sdk/pull/5587) + * Fix saving the collapsed state of the left panel + [\#5593](https://github.com/matrix-org/matrix-react-sdk/pull/5593) + * Fix app-url hint in the e2e-test run script output + [\#5600](https://github.com/matrix-org/matrix-react-sdk/pull/5600) + * Fix RoomView re-mounting breaking peeking + [\#5602](https://github.com/matrix-org/matrix-react-sdk/pull/5602) + * Tweak a few room ID checks + [\#5592](https://github.com/matrix-org/matrix-react-sdk/pull/5592) + * Remove pills from event permalinks with text + [\#5575](https://github.com/matrix-org/matrix-react-sdk/pull/5575) + +Changes in [3.13.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.1) (2021-02-04) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0...v3.13.1) + + * [Release] Fix z-index of stickerpicker + [\#5618](https://github.com/matrix-org/matrix-react-sdk/pull/5618) + +Changes in [3.13.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0) (2021-02-03) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0-rc.1...v3.13.0) + + * Upgrade to JS SDK 9.6.0 + * [Release] Fix flair height after accent changes + [\#5612](https://github.com/matrix-org/matrix-react-sdk/pull/5612) + * [Release] Iterate Social Logins work around edge cases and branding + [\#5610](https://github.com/matrix-org/matrix-react-sdk/pull/5610) + * [Release] Lock widget room ID when added + [\#5608](https://github.com/matrix-org/matrix-react-sdk/pull/5608) + * [Release] Better errors for SSO failures + [\#5606](https://github.com/matrix-org/matrix-react-sdk/pull/5606) + * [Release] Fix RoomView re-mounting breaking peeking + [\#5603](https://github.com/matrix-org/matrix-react-sdk/pull/5603) + +Changes in [3.13.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0-rc.1) (2021-01-29) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.1...v3.13.0-rc.1) + + * Upgrade to JS SDK 9.6.0-rc.1 + * Translations update from Weblate + [\#5597](https://github.com/matrix-org/matrix-react-sdk/pull/5597) + * Support managed hybrid widgets from config + [\#5596](https://github.com/matrix-org/matrix-react-sdk/pull/5596) + * Add managed hybrid call widgets when supported + [\#5594](https://github.com/matrix-org/matrix-react-sdk/pull/5594) + * Tweak mobile guide toast copy + [\#5595](https://github.com/matrix-org/matrix-react-sdk/pull/5595) + * Improve SSO auth flow + [\#5578](https://github.com/matrix-org/matrix-react-sdk/pull/5578) + * Add optional mobile guide toast + [\#5586](https://github.com/matrix-org/matrix-react-sdk/pull/5586) + * Fix invisible text after logging out in the dark theme + [\#5588](https://github.com/matrix-org/matrix-react-sdk/pull/5588) + * Fix escape for cancelling replies + [\#5591](https://github.com/matrix-org/matrix-react-sdk/pull/5591) + * Update widget-api to beta.12 + [\#5589](https://github.com/matrix-org/matrix-react-sdk/pull/5589) + * Add commands for DM conversion + [\#5540](https://github.com/matrix-org/matrix-react-sdk/pull/5540) + * Run a UI refresh over the OIDC Exchange confirmation dialog + [\#5580](https://github.com/matrix-org/matrix-react-sdk/pull/5580) + * Allow stickerpickers the legacy "visibility" capability + [\#5581](https://github.com/matrix-org/matrix-react-sdk/pull/5581) + * Hide local video if it is muted + [\#5529](https://github.com/matrix-org/matrix-react-sdk/pull/5529) + * Don't use name width in reply thread for IRC layout + [\#5518](https://github.com/matrix-org/matrix-react-sdk/pull/5518) + * Update code_style.md + [\#5554](https://github.com/matrix-org/matrix-react-sdk/pull/5554) + * Fix Czech capital letters like ŠČŘ... + [\#5569](https://github.com/matrix-org/matrix-react-sdk/pull/5569) + * Add optional search shortcut + [\#5548](https://github.com/matrix-org/matrix-react-sdk/pull/5548) + * Fix Sudden 'find a room' UI shows up when the only room moves to favourites + [\#5584](https://github.com/matrix-org/matrix-react-sdk/pull/5584) + * Increase PersistedElement's z-index + [\#5568](https://github.com/matrix-org/matrix-react-sdk/pull/5568) + * Remove check that prevents Jitsi widgets from being unpinned + [\#5582](https://github.com/matrix-org/matrix-react-sdk/pull/5582) + * Fix Jitsi widgets causing localized tile crashes + [\#5583](https://github.com/matrix-org/matrix-react-sdk/pull/5583) + * Log candidates for calls + [\#5573](https://github.com/matrix-org/matrix-react-sdk/pull/5573) + * Upgrade deps 2021-01 + [\#5579](https://github.com/matrix-org/matrix-react-sdk/pull/5579) + * Fix "Continuing without email" dialog bug + [\#5566](https://github.com/matrix-org/matrix-react-sdk/pull/5566) + * Require registration for verification actions + [\#5574](https://github.com/matrix-org/matrix-react-sdk/pull/5574) + * Don't play the hangup sound when the call is answered from elsewhere + [\#5572](https://github.com/matrix-org/matrix-react-sdk/pull/5572) + * Move to newer base image for end-to-end tests + [\#5570](https://github.com/matrix-org/matrix-react-sdk/pull/5570) + * Update widgets in the room upon join + [\#5564](https://github.com/matrix-org/matrix-react-sdk/pull/5564) + * Update AuxPanel and related buttons when widgets change or on reload + [\#5563](https://github.com/matrix-org/matrix-react-sdk/pull/5563) + * Add VoIP user mapper + [\#5560](https://github.com/matrix-org/matrix-react-sdk/pull/5560) + * Improve styling of SSO Buttons for multiple IdPs + [\#5558](https://github.com/matrix-org/matrix-react-sdk/pull/5558) + * Fixes for the general tab in the room dialog + [\#5522](https://github.com/matrix-org/matrix-react-sdk/pull/5522) + * fix issue 16226 to allow switching back to default HS. + [\#5561](https://github.com/matrix-org/matrix-react-sdk/pull/5561) + * Support room-defined widget layouts + [\#5553](https://github.com/matrix-org/matrix-react-sdk/pull/5553) + * Change a bunch of strings from Recovery Key/Phrase to Security Key/Phrase + [\#5533](https://github.com/matrix-org/matrix-react-sdk/pull/5533) + * Give a bigger target area to AppsDrawer vertical resizer + [\#5557](https://github.com/matrix-org/matrix-react-sdk/pull/5557) + * Fix minimized left panel avatar alignment + [\#5493](https://github.com/matrix-org/matrix-react-sdk/pull/5493) + * Ensure component index has been written before renaming + [\#5556](https://github.com/matrix-org/matrix-react-sdk/pull/5556) + * Fixed continue button while selecting home-server + [\#5552](https://github.com/matrix-org/matrix-react-sdk/pull/5552) + * Wire up MSC2931 widget navigation + [\#5527](https://github.com/matrix-org/matrix-react-sdk/pull/5527) + * Various fixes for Bridge Info page (MSC2346) + [\#5454](https://github.com/matrix-org/matrix-react-sdk/pull/5454) + * Use room-specific listeners for message preview and community prototype + [\#5547](https://github.com/matrix-org/matrix-react-sdk/pull/5547) + * Fix some misc. React warnings when viewing timeline + [\#5546](https://github.com/matrix-org/matrix-react-sdk/pull/5546) + * Use device storage for allowed widgets if account data not supported + [\#5544](https://github.com/matrix-org/matrix-react-sdk/pull/5544) + * Fix incoming call box on dark theme + [\#5542](https://github.com/matrix-org/matrix-react-sdk/pull/5542) + * Convert DMRoomMap to typescript + [\#5541](https://github.com/matrix-org/matrix-react-sdk/pull/5541) + * Add in-call dialpad for DTMF sending + [\#5532](https://github.com/matrix-org/matrix-react-sdk/pull/5532) + +Changes in [3.12.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.1) (2021-01-26) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.0...v3.12.1) + + * Upgrade to JS SDK 9.5.1 + +Changes in [3.12.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0) (2021-01-18) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.0-rc.1...v3.12.0) + + * Upgrade to JS SDK 9.5.0 + * Fix incoming call box on dark theme + [\#5543](https://github.com/matrix-org/matrix-react-sdk/pull/5543) + +Changes in [3.12.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0-rc.1) (2021-01-13) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.1...v3.12.0-rc.1) + + * Upgrade to JS SDK 9.5.0-rc.1 + * Fix soft crash on soft logout page + [\#5539](https://github.com/matrix-org/matrix-react-sdk/pull/5539) + * Translations update from Weblate + [\#5538](https://github.com/matrix-org/matrix-react-sdk/pull/5538) + * Run TypeScript tests + [\#5537](https://github.com/matrix-org/matrix-react-sdk/pull/5537) + * Add a basic widget explorer to devtools (per-room) + [\#5528](https://github.com/matrix-org/matrix-react-sdk/pull/5528) + * Add to security key field + [\#5534](https://github.com/matrix-org/matrix-react-sdk/pull/5534) + * Fix avatar upload prompt/tooltip floating wrong and permissions + [\#5526](https://github.com/matrix-org/matrix-react-sdk/pull/5526) + * Add a dialpad UI for PSTN lookup + [\#5523](https://github.com/matrix-org/matrix-react-sdk/pull/5523) + * Basic call transfer initiation support + [\#5494](https://github.com/matrix-org/matrix-react-sdk/pull/5494) + * Fix #15988 + [\#5524](https://github.com/matrix-org/matrix-react-sdk/pull/5524) + * Bump node-notifier from 8.0.0 to 8.0.1 + [\#5520](https://github.com/matrix-org/matrix-react-sdk/pull/5520) + * Use TypeScript source for development, swap to build during release + [\#5503](https://github.com/matrix-org/matrix-react-sdk/pull/5503) + * Look for emoji in the body that will be displayed + [\#5517](https://github.com/matrix-org/matrix-react-sdk/pull/5517) + * Bump ini from 1.3.5 to 1.3.7 + [\#5486](https://github.com/matrix-org/matrix-react-sdk/pull/5486) + * Recognise `*.element.io` links as Element permalinks + [\#5514](https://github.com/matrix-org/matrix-react-sdk/pull/5514) + * Fixes for call UI + [\#5509](https://github.com/matrix-org/matrix-react-sdk/pull/5509) + * Add a snowfall chat effect (with /snowfall command) + [\#5511](https://github.com/matrix-org/matrix-react-sdk/pull/5511) + * fireworks effect + [\#5507](https://github.com/matrix-org/matrix-react-sdk/pull/5507) + * Don't play call end sound for calls that never started + [\#5506](https://github.com/matrix-org/matrix-react-sdk/pull/5506) + * Add /tableflip slash command + [\#5485](https://github.com/matrix-org/matrix-react-sdk/pull/5485) + * Import from src in IncomingCallBox.tsx + [\#5504](https://github.com/matrix-org/matrix-react-sdk/pull/5504) + * Social Login support both https and mxc icons + [\#5499](https://github.com/matrix-org/matrix-react-sdk/pull/5499) + * Fix padding in confirmation email registration prompt + [\#5501](https://github.com/matrix-org/matrix-react-sdk/pull/5501) + * Fix room list help prompt alignment + [\#5500](https://github.com/matrix-org/matrix-react-sdk/pull/5500) + +Changes in [3.11.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.1) (2020-12-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0...v3.11.1) + + * Upgrade JS SDK to 9.4.1 + +Changes in [3.11.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.0) (2020-12-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0-rc.2...v3.11.0) + + * Upgrade JS SDK to 9.4.0 + * [Release] Look for emoji in the body that will be displayed + [\#5519](https://github.com/matrix-org/matrix-react-sdk/pull/5519) + * [Release] Recognise `*.element.io` links as Element permalinks + [\#5516](https://github.com/matrix-org/matrix-react-sdk/pull/5516) + * [Release] Fixes for call UI + [\#5513](https://github.com/matrix-org/matrix-react-sdk/pull/5513) + * [RELEASE] Add a snowfall chat effect (with /snowfall command) + [\#5512](https://github.com/matrix-org/matrix-react-sdk/pull/5512) + * [Release] Fix padding in confirmation email registration prompt + [\#5502](https://github.com/matrix-org/matrix-react-sdk/pull/5502) + +Changes in [3.11.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.0-rc.2) (2020-12-16) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0-rc.1...v3.11.0-rc.2) + + * Upgrade JS SDK to 9.4.0-rc.2 + +Changes in [3.11.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.0-rc.1) (2020-12-16) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.10.0...v3.11.0-rc.1) + + * Upgrade JS SDK to 9.4.0-rc.1 + * Translations update from Weblate + [\#5497](https://github.com/matrix-org/matrix-react-sdk/pull/5497) + * Unregister from the dispatcher in CallHandler + [\#5495](https://github.com/matrix-org/matrix-react-sdk/pull/5495) + * Better adhere to MSC process + [\#5496](https://github.com/matrix-org/matrix-react-sdk/pull/5496) + * Use random pickle key on all platforms + [\#5483](https://github.com/matrix-org/matrix-react-sdk/pull/5483) + * Fix mx_MemberList icons + [\#5492](https://github.com/matrix-org/matrix-react-sdk/pull/5492) + * Convert InviteDialog to TypeScript + [\#5491](https://github.com/matrix-org/matrix-react-sdk/pull/5491) + * Add keyboard shortcut for emoji reactions + [\#5425](https://github.com/matrix-org/matrix-react-sdk/pull/5425) + * Run chat effects on events sent by widgets too + [\#5488](https://github.com/matrix-org/matrix-react-sdk/pull/5488) + * Fix being unable to pin widgets + [\#5487](https://github.com/matrix-org/matrix-react-sdk/pull/5487) + * Line 1 / 2 Support + [\#5468](https://github.com/matrix-org/matrix-react-sdk/pull/5468) + * Remove impossible labs feature: sending hidden read receipts + [\#5484](https://github.com/matrix-org/matrix-react-sdk/pull/5484) + * Fix height of Remote Video in call + [\#5456](https://github.com/matrix-org/matrix-react-sdk/pull/5456) + * Add UI for hold functionality + [\#5446](https://github.com/matrix-org/matrix-react-sdk/pull/5446) + * Allow SearchBox to expand to fill width + [\#5411](https://github.com/matrix-org/matrix-react-sdk/pull/5411) + * Use room alias in generated permalink for rooms + [\#5451](https://github.com/matrix-org/matrix-react-sdk/pull/5451) + * Only show confetti if the current room is receiving an appropriate event + [\#5482](https://github.com/matrix-org/matrix-react-sdk/pull/5482) + * Throttle RoomState.members handler to improve performance + [\#5481](https://github.com/matrix-org/matrix-react-sdk/pull/5481) + * Handle manual hs urls better for the server picker + [\#5477](https://github.com/matrix-org/matrix-react-sdk/pull/5477) + * Add Olm as a dev dependency for types + [\#5479](https://github.com/matrix-org/matrix-react-sdk/pull/5479) + * Hide Invite to this room CTA if no permission + [\#5476](https://github.com/matrix-org/matrix-react-sdk/pull/5476) + * Fix width of underline in server picker dialog + [\#5478](https://github.com/matrix-org/matrix-react-sdk/pull/5478) + * Fix confetti room unread state check + [\#5475](https://github.com/matrix-org/matrix-react-sdk/pull/5475) + * Show confetti in a chat room on command or emoji + [\#5140](https://github.com/matrix-org/matrix-react-sdk/pull/5140) + * Fix inverted settings default value + [\#5391](https://github.com/matrix-org/matrix-react-sdk/pull/5391) + * Improve usability of the Server Picker Dialog + [\#5474](https://github.com/matrix-org/matrix-react-sdk/pull/5474) + * Fix typos in some strings + [\#5473](https://github.com/matrix-org/matrix-react-sdk/pull/5473) + * Bump highlight.js from 10.1.2 to 10.4.1 + [\#5472](https://github.com/matrix-org/matrix-react-sdk/pull/5472) + * Remove old app test script path + [\#5471](https://github.com/matrix-org/matrix-react-sdk/pull/5471) + * add support for giving reason when redacting + [\#5260](https://github.com/matrix-org/matrix-react-sdk/pull/5260) + * Add support for Netlify to fetchdep script + [\#5469](https://github.com/matrix-org/matrix-react-sdk/pull/5469) + * Nest other layers inside on automation + [\#5467](https://github.com/matrix-org/matrix-react-sdk/pull/5467) + * Rebrand various CI scripts and modules + [\#5466](https://github.com/matrix-org/matrix-react-sdk/pull/5466) + * Add more widget sanity checking + [\#5462](https://github.com/matrix-org/matrix-react-sdk/pull/5462) + * Fix React complaining about unknown DOM props + [\#5465](https://github.com/matrix-org/matrix-react-sdk/pull/5465) + * Jump to home page when leaving a room + [\#5464](https://github.com/matrix-org/matrix-react-sdk/pull/5464) + * Fix SSO buttons for Social Logins + [\#5463](https://github.com/matrix-org/matrix-react-sdk/pull/5463) + * Social Login and login delight tweaks + [\#5426](https://github.com/matrix-org/matrix-react-sdk/pull/5426) + Changes in [3.10.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0) (2020-12-07) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.10.0-rc.1...v3.10.0) diff --git a/babel.config.js b/babel.config.js index d5a97d56ce..0a3a34a391 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,12 +3,15 @@ module.exports = { "presets": [ ["@babel/preset-env", { "targets": [ - "last 2 Chrome versions", "last 2 Firefox versions", "last 2 Safari versions" + "last 2 Chrome versions", + "last 2 Firefox versions", + "last 2 Safari versions", + "last 2 Edge versions", ], }], "@babel/preset-typescript", "@babel/preset-flow", - "@babel/preset-react" + "@babel/preset-react", ], "plugins": [ ["@babel/plugin-proposal-decorators", {legacy: true}], @@ -18,6 +21,6 @@ module.exports = { "@babel/plugin-proposal-object-rest-spread", "@babel/plugin-transform-flow-comments", "@babel/plugin-syntax-dynamic-import", - "@babel/plugin-transform-runtime" - ] + "@babel/plugin-transform-runtime", + ], }; diff --git a/code_style.md b/code_style.md index fe04d2cc3d..5747540a76 100644 --- a/code_style.md +++ b/code_style.md @@ -35,12 +35,6 @@ General Style - lowerCamelCase for functions and variables. - Single line ternary operators are fine. - UPPER_SNAKE_CASE for constants -- Single quotes for strings by default, for consistency with most JavaScript styles: - - ```javascript - "bad" // Bad - 'good' // Good - ``` - Use parentheses or `` ` `` instead of `\` for line continuation where ever possible - Open braces on the same line (consistent with Node): @@ -162,7 +156,14 @@ ECMAScript - Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an arrow function, they probably all should be. - Apart from that, newer ES features should be used whenever the author deems them to be appropriate. -- Flow annotations are welcome and encouraged. + +TypeScript +---------- +- TypeScript is preferred over the use of JavaScript +- It's desirable to convert existing JavaScript files to TypeScript. TypeScript conversions should be done in small + chunks without functional changes to ease the review process. +- Use full type definitions for function parameters and return values. +- Avoid `any` types and `any` casts React ----- @@ -201,6 +202,8 @@ React this.state = { counter: 0 }; } ``` +- Prefer class components over function components and hooks (not a strict rule though) + - Think about whether your component really needs state: are you duplicating information in component state that could be derived from the model? diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index f522dc2fc4..379b6f5b51 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -21,14 +21,14 @@ caret nodes (more on that later). For these reasons it doesn't use `innerText`, `textContent` or anything similar. The model addresses any content in the editor within as an offset within this string. The caret position is thus also converted from a position in the DOM tree -to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.js`. +to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.ts`. Once the content string and caret offset is calculated, it is passed to the `update()` method of the model. The model first calculates the same content string of its current parts, basically just concatenating their text. It then looks for differences between the current and the new content string. The diffing algorithm is very basic, and assumes there is only one change around the caret offset, -so this should be very inexpensive. See `diff.js` for details. +so this should be very inexpensive. See `diff.ts` for details. The result of the diffing is the strings that were added and/or removed from the current content. These differences are then applied to the parts, @@ -51,7 +51,7 @@ which relate poorly to text input or changes, and don't need the `beforeinput` e which isn't broadly supported yet. Once the parts of the model are updated, the DOM of the editor is then reconciled -with the new model state, see `renderModel` in `render.js` for this. +with the new model state, see `renderModel` in `render.ts` for this. If the model didn't reject the input and didn't make any additional changes, this won't make any changes to the DOM at all, and should thus be fairly efficient. diff --git a/docs/media-handling.md b/docs/media-handling.md new file mode 100644 index 0000000000..a4307fb7d4 --- /dev/null +++ b/docs/media-handling.md @@ -0,0 +1,19 @@ +# Media handling + +Surely media should be as easy as just putting a URL into an `img` and calling it good, right? +Not quite. Matrix uses something called a Matrix Content URI (better known as MXC URI) to identify +content, which is then converted to a regular HTTPS URL on the homeserver. However, sometimes that +URL can change depending on deployment considerations. + +The react-sdk features a [customisation endpoint](https://github.com/vector-im/element-web/blob/develop/docs/customisations.md) +for media handling where all conversions from MXC URI to HTTPS URL happen. This is to ensure that +those obscure deployments can route all their media to the right place. + +For development, there are currently two functions available: `mediaFromMxc` and `mediaFromContent`. +The `mediaFromMxc` function should be self-explanatory. `mediaFromContent` takes an event content as +a parameter and will automatically parse out the source media and thumbnail. Both functions return +a `Media` object with a number of options on it, such as getting various common HTTPS URLs for the +media. + +**It is extremely important that all media calls are put through this customisation endpoint.** So +much so it's a lint rule to avoid accidental use of the wrong functions. diff --git a/docs/widget-layouts.md b/docs/widget-layouts.md new file mode 100644 index 0000000000..e7f72e2001 --- /dev/null +++ b/docs/widget-layouts.md @@ -0,0 +1,60 @@ +# Widget layout support + +Rooms can have a default widget layout to auto-pin certain widgets, make the container different +sizes, etc. These are defined through the `io.element.widgets.layout` state event (empty state key). + +Full example content: +```json5 +{ + "widgets": { + "first-widget-id": { + "container": "top", + "index": 0, + "width": 60, + "height": 40 + }, + "second-widget-id": { + "container": "right" + } + } +} +``` + +As shown, there are two containers possible for widgets. These containers have different behaviour +and interpret the other options differently. + +## `top` container + +This is the "App Drawer" or any pinned widgets in a room. This is by far the most versatile container +though does introduce potential usability issues upon members of the room (widgets take up space and +therefore fewer messages can be shown). + +The `index` for a widget determines which order the widgets show up in from left to right. Widgets +without an `index` will show up as the rightmost widgets. Tiebreaks (same `index` or multiple defined +without an `index`) are resolved by comparing widget IDs. A maximum of 3 widgets can be in the top +container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers +represent leftmost widgets. + +The `width` is relative width within the container in percentage points. This will be clamped to a +range of 0-100 (inclusive). The widgets will attempt to scale to relative proportions when more than +100% space is allocated. For example, if 3 widgets are defined at 40% width each then the client will +attempt to show them at 33% width each. + +Note that the client may impose minimum widths on the widgets, such as a 10% minimum to avoid pinning +hidden widgets. In general, widgets defined in the 30-70% range each will be free of these restrictions. + +The `height` is not in fact applied per-widget but is recorded per-widget for potential future +capabilities in future containers. The top container will take the tallest `height` and use that for +the height of the whole container, and thus all widgets in that container. The `height` is relative +to the container, like with `width`, meaning that 100% will consume as much space as the client is +willing to sacrifice to the widget container. Like with `width`, the client may impose minimums to avoid +the container being uselessly small. Heights in the 30-100% range are generally acceptable. The height +is also clamped to be within 0-100, inclusive. + +## `right` container + +This is the default container and has no special configuration. Widgets which overflow from the top +container will be put in this container instead. Putting a widget in the right container does not +automatically show it - it only mentions that widgets should not be in another container. + +The behaviour of this container may change in the future. diff --git a/package.json b/package.json index b328823b24..6a8645adf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.10.0", + "version": "3.17.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -27,11 +27,12 @@ "matrix-gen-i18n": "scripts/gen-i18n.js", "matrix-prune-i18n": "scripts/prune-i18n.js" }, - "main": "./lib/index.js", - "typings": "./lib/index.d.ts", + "main": "./src/index.js", "matrix_src_main": "./src/index.js", + "matrix_lib_main": "./lib/index.js", + "matrix_lib_typings": "./lib/index.d.ts", "scripts": { - "prepare": "yarn build", + "prepublishOnly": "yarn build", "i18n": "matrix-gen-i18n", "prunei18n": "matrix-prune-i18n", "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", @@ -53,48 +54,47 @@ "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080" }, "dependencies": { - "@babel/runtime": "^7.10.5", - "await-lock": "^2.0.1", - "blueimp-canvas-to-blob": "^3.27.0", + "@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.3", + "cheerio": "^1.0.0-rc.5", "classnames": "^2.2.6", - "commonmark": "^0.29.1", + "commonmark": "^0.29.3", "counterpart": "^0.18.6", - "diff-dom": "^4.1.6", + "diff-dom": "^4.2.2", "diff-match-patch": "^1.0.5", - "emojibase-data": "^5.0.1", - "emojibase-regex": "^4.0.1", + "emojibase-data": "^5.1.1", + "emojibase-regex": "^4.1.1", "escape-html": "^1.0.3", - "file-saver": "^1.3.8", - "filesize": "3.6.1", + "file-saver": "^2.0.5", + "filesize": "6.1.0", "flux": "2.1.1", - "focus-visible": "^5.1.0", - "fuse.js": "^2.7.4", + "focus-visible": "^5.2.0", "gfm.css": "^1.1.2", "glob-to-regexp": "^0.4.1", - "highlight.js": "^10.1.2", - "html-entities": "^1.3.1", - "is-ip": "^2.0.0", + "highlight.js": "^10.5.0", + "html-entities": "^1.4.0", + "is-ip": "^3.1.0", "katex": "^0.12.0", "linkifyjs": "^2.1.9", - "lodash": "^4.17.19", + "lodash": "^4.17.20", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.10", + "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", - "pako": "^1.0.11", - "parse5": "^5.1.1", + "opus-recorder": "^8.0.3", + "pako": "^2.0.3", + "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", - "project-name-generator": "^2.1.7", "prop-types": "^15.7.2", "qrcode": "^1.4.4", - "qs": "^6.9.4", - "re-resizable": "^6.5.4", - "react": "^16.13.1", + "qs": "^6.9.6", + "re-resizable": "^6.9.0", + "react": "^16.14.0", "react-beautiful-dnd": "^4.0.1", - "react-dom": "^16.13.1", - "react-focus-lock": "^2.4.1", + "react-dom": "^16.14.0", + "react-focus-lock": "^2.5.0", "react-transition-group": "^4.4.1", "resize-observer-polyfill": "^1.5.1", "rfc4648": "^1.4.0", @@ -102,76 +102,81 @@ "tar-js": "^0.3.0", "text-encoding-utf-8": "^1.0.2", "url": "^0.11.0", - "velocity-animate": "^1.5.2", + "velocity-animate": "^2.0.6", "what-input": "^5.2.10", "zxcvbn": "^4.4.2" }, "devDependencies": { - "@babel/cli": "^7.10.5", - "@babel/core": "^7.10.5", - "@babel/parser": "^7.11.0", - "@babel/plugin-proposal-class-properties": "^7.10.4", - "@babel/plugin-proposal-decorators": "^7.10.5", - "@babel/plugin-proposal-export-default-from": "^7.10.4", - "@babel/plugin-proposal-numeric-separator": "^7.10.4", - "@babel/plugin-proposal-object-rest-spread": "^7.10.4", - "@babel/plugin-transform-flow-comments": "^7.10.4", - "@babel/plugin-transform-runtime": "^7.10.5", - "@babel/preset-env": "^7.10.4", - "@babel/preset-flow": "^7.10.4", - "@babel/preset-react": "^7.10.4", - "@babel/preset-typescript": "^7.10.4", - "@babel/register": "^7.10.5", - "@babel/traverse": "^7.11.0", - "@peculiar/webcrypto": "^1.1.3", - "@types/classnames": "^2.2.10", + "@babel/cli": "^7.12.10", + "@babel/core": "^7.12.10", + "@babel/parser": "^7.12.11", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-decorators": "^7.12.12", + "@babel/plugin-proposal-export-default-from": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.7", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-transform-flow-comments": "^7.12.1", + "@babel/plugin-transform-runtime": "^7.12.10", + "@babel/preset-env": "^7.12.11", + "@babel/preset-flow": "^7.12.1", + "@babel/preset-react": "^7.12.10", + "@babel/preset-typescript": "^7.12.7", + "@babel/register": "^7.12.10", + "@babel/traverse": "^7.12.12", + "@peculiar/webcrypto": "^1.1.4", + "@sinonjs/fake-timers": "^7.0.2", + "@types/classnames": "^2.2.11", "@types/counterpart": "^0.18.1", "@types/flux": "^3.1.9", + "@types/jest": "^26.0.20", "@types/linkifyjs": "^2.1.3", - "@types/lodash": "^4.14.158", + "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", - "@types/node": "^12.12.51", + "@types/node": "^14.14.22", "@types/pako": "^1.0.1", - "@types/qrcode": "^1.3.4", + "@types/qrcode": "^1.3.5", "@types/react": "^16.9", - "@types/react-dom": "^16.9.8", + "@types/react-dom": "^16.9.10", "@types/react-transition-group": "^4.4.0", - "@types/sanitize-html": "^1.23.3", + "@types/sanitize-html": "^1.27.0", "@types/zxcvbn": "^4.4.0", - "@typescript-eslint/eslint-plugin": "^3.7.0", - "@typescript-eslint/parser": "^3.7.0", + "@typescript-eslint/eslint-plugin": "^4.14.0", + "@typescript-eslint/parser": "^4.14.0", "babel-eslint": "^10.1.0", - "babel-jest": "^24.9.0", - "chokidar": "^3.4.1", - "concurrently": "^4.1.2", + "babel-jest": "^26.6.3", + "chokidar": "^3.5.1", + "concurrently": "^5.3.0", "enzyme": "^3.11.0", - "enzyme-adapter-react-16": "^1.15.2", - "eslint": "7.5.0", - "eslint-config-matrix-org": "^0.1.2", + "enzyme-adapter-react-16": "^1.15.6", + "eslint": "7.18.0", + "eslint-config-matrix-org": "^0.2.0", "eslint-plugin-babel": "^5.3.1", - "eslint-plugin-flowtype": "^2.50.3", - "eslint-plugin-react": "^7.20.3", - "eslint-plugin-react-hooks": "^2.5.1", - "glob": "^5.0.15", - "jest": "^26.5.2", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-react": "^7.22.0", + "eslint-plugin-react-hooks": "^4.2.0", + "glob": "^7.1.6", + "jest": "^26.6.3", "jest-canvas-mock": "^2.3.0", "jest-environment-jsdom-sixteen": "^1.0.3", - "lolex": "^5.1.2", + "jest-fetch-mock": "^3.0.3", "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", - "react-test-renderer": "^16.13.1", - "rimraf": "^2.7.1", - "stylelint": "^9.10.1", - "stylelint-config-standard": "^18.3.0", + "react-test-renderer": "^16.14.0", + "rimraf": "^3.0.2", + "stylelint": "^13.9.0", + "stylelint-config-standard": "^20.0.0", "stylelint-scss": "^3.18.0", - "typescript": "^3.9.7", + "typescript": "^4.1.3", "walk": "^2.3.14" }, + "resolutions": { + "**/@types/react": "^16.14" + }, "jest": { "testEnvironment": "./__test-utils__/environment.js", "testMatch": [ - "/test/**/*-test.js" + "/test/**/*-test.[jt]s" ], "setupFiles": [ "jest-canvas-mock" diff --git a/res/css/_common.scss b/res/css/_common.scss index 87336a1c03..0093bde0ab 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -21,6 +21,11 @@ limitations under the License. $hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic +$EventTile_e2e_state_indicator_width: 4px; + +$MessageTimestamp_width: 46px; /* 8 + 30 (avatar) + 8 */ +$MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e_state_indicator_width); + :root { font-size: 10px; } @@ -390,6 +395,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border: 1px solid $accent-color; color: $accent-color; background-color: $button-secondary-bg-color; + font-family: inherit; } .mx_Dialog button:last-child { @@ -484,54 +490,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { margin-top: 69px; } -.mx_Beta { - color: red; - margin-right: 10px; - position: relative; - top: -3px; - background-color: white; - padding: 0 4px; - border-radius: 3px; - border: 1px solid darkred; - cursor: help; - transition-duration: 200ms; - font-size: smaller; - filter: opacity(0.5); -} - -.mx_Beta:hover { - color: white; - border: 1px solid gray; - background-color: darkred; -} - -.mx_TintableSvgButton { - position: relative; - display: flex; - flex-direction: row; - justify-content: center; - align-content: center; -} - -.mx_TintableSvgButton object { - margin: 0; - padding: 0; - width: 100%; - height: 100%; - max-width: 100%; - max-height: 100%; -} - -.mx_TintableSvgButton span { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - opacity: 0; - cursor: pointer; -} - // username colors // used by SenderProfile & RoomPreviewBar .mx_Username_color1 { @@ -601,6 +559,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } } +@define-mixin ProgressBarBgColour $colour { + background-color: $colour; + &::-webkit-progress-bar { + background-color: $colour; + } +} + @define-mixin ProgressBarBorderRadius $radius { border-radius: $radius; &::-moz-progress-bar { diff --git a/res/css/_components.scss b/res/css/_components.scss index d8bc238db5..215b6605a5 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -27,6 +27,9 @@ @import "./structures/_RoomView.scss"; @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; +@import "./structures/_SpacePanel.scss"; +@import "./structures/_SpaceRoomDirectory.scss"; +@import "./structures/_SpaceRoomView.scss"; @import "./structures/_TabbedView.scss"; @import "./structures/_ToastContainer.scss"; @import "./structures/_UploadBar.scss"; @@ -56,6 +59,7 @@ @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; +@import "./views/dialogs/_AddExistingToSpaceDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_BugReportDialog.scss"; @@ -71,6 +75,7 @@ @import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; +@import "./views/dialogs/_HostSignupDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @@ -88,6 +93,7 @@ @import "./views/dialogs/_SettingsDialog.scss"; @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_SlashCommandHelpDialog.scss"; +@import "./views/dialogs/_SpaceSettingsDialog.scss"; @import "./views/dialogs/_TabbedIntegrationManagerDialog.scss"; @import "./views/dialogs/_TermsDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss"; @@ -105,11 +111,13 @@ @import "./views/elements/_AddressSelector.scss"; @import "./views/elements/_AddressTile.scss"; @import "./views/elements/_DesktopBuildsNotice.scss"; +@import "./views/elements/_DesktopCapturerSourcePicker.scss"; @import "./views/elements/_DirectorySearchBox.scss"; @import "./views/elements/_Dropdown.scss"; @import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; +@import "./views/elements/_FacePile.scss"; @import "./views/elements/_Field.scss"; @import "./views/elements/_FormButton.scss"; @import "./views/elements/_ImageView.scss"; @@ -204,6 +212,7 @@ @import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss"; +@import "./views/rooms/_VoiceRecordComposerTile.scss"; @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_AvatarSetting.scss"; @import "./views/settings/_CrossSigningPanel.scss"; @@ -217,6 +226,7 @@ @import "./views/settings/_SecureBackupPanel.scss"; @import "./views/settings/_SetIdServer.scss"; @import "./views/settings/_SetIntegrationManager.scss"; +@import "./views/settings/_SpellCheckLanguages.scss"; @import "./views/settings/_UpdateCheckButton.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; @@ -230,10 +240,17 @@ @import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; +@import "./views/spaces/_SpaceBasicSettings.scss"; +@import "./views/spaces/_SpaceCreateMenu.scss"; +@import "./views/spaces/_SpacePublicShare.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/toasts/_AnalyticsToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; +@import "./views/voice_messages/_Waveform.scss"; @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; +@import "./views/voip/_DialPad.scss"; +@import "./views/voip/_DialPadContextMenu.scss"; +@import "./views/voip/_DialPadModal.scss"; @import "./views/voip/_VideoFeed.scss"; diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 1424d9cda0..7c3cd1c513 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -15,10 +15,12 @@ limitations under the License. */ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculations +$roomListCollapsedWidth: 68px; .mx_LeftPanel { background-color: $roomlist-bg-color; - min-width: 260px; + // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel + min-width: 206px; max-width: 50%; // Create a row-based flexbox for the GroupFilterPanel and the room list @@ -37,18 +39,12 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation // GroupFilterPanel handles its own CSS } - &:not(.mx_LeftPanel_hasGroupFilterPanel) { - .mx_LeftPanel_roomListContainer { - width: 100%; - } - } - // Note: The 'room list' in this context is actually everything that isn't the tag // panel, such as the menu options, breadcrumbs, filtering, etc .mx_LeftPanel_roomListContainer { - width: calc(100% - $groupFilterPanelWidth); background-color: $roomlist-bg-color; - + flex: 1 0 0; + min-width: 0; // Create another flexbox (this time a column) for the room list components display: flex; flex-direction: column; @@ -134,6 +130,10 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation mask-repeat: no-repeat; background: $secondary-fg-color; } + + &.mx_LeftPanel_exploreButton_space::before { + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); + } } } @@ -168,17 +168,15 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation // These styles override the defaults for the minimized (66px) layout &.mx_LeftPanel_minimized { min-width: unset; - - // We have to forcefully set the width to override the resizer's style attribute. - &.mx_LeftPanel_hasGroupFilterPanel { - width: calc(68px + $groupFilterPanelWidth) !important; - } - &:not(.mx_LeftPanel_hasGroupFilterPanel) { - width: 68px !important; - } + width: unset !important; .mx_LeftPanel_roomListContainer { - width: 68px; + width: $roomListCollapsedWidth; + + .mx_LeftPanel_userHeader { + flex-direction: row; + justify-content: center; + } .mx_LeftPanel_filterContainer { // Organize the flexbox into a centered column layout diff --git a/res/css/structures/_LeftPanelWidget.scss b/res/css/structures/_LeftPanelWidget.scss index 4df651d7b6..6e2d99bb37 100644 --- a/res/css/structures/_LeftPanelWidget.scss +++ b/res/css/structures/_LeftPanelWidget.scss @@ -134,7 +134,7 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; - mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); + mask-image: url('$(res)/img/feather-customised/maximise.svg'); background: $muted-fg-color; } } diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index ad1656efbb..8199121420 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -18,6 +18,7 @@ limitations under the License. display: flex; flex-direction: row; min-width: 0; + min-height: 0; height: 100%; } diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 812a7f8472..a220c5d505 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -66,7 +66,7 @@ limitations under the License. } /* not the left panel, and not the resize handle, so the roomview/groupview/... */ -.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_ResizeHandle) { +.mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_SpacePanel):not(.mx_ResizeHandle) { background-color: $primary-bg-color; flex: 1 1 0; diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 5bf0d953f3..5515fe4060 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -160,3 +160,20 @@ limitations under the License. mask-position: center; } } + +.mx_RightPanel_scopeHeader { + margin: 24px; + text-align: center; + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + + .mx_BaseAvatar { + margin-right: 8px; + vertical-align: middle; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } +} diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 29e6fecd34..89cb21b7a6 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -64,28 +64,23 @@ limitations under the License. } .mx_RoomDirectory_table { - font-size: $font-12px; color: $primary-fg-color; - width: 100%; + display: grid; + font-size: $font-12px; + grid-template-columns: max-content auto max-content max-content max-content; + row-gap: 24px; text-align: left; - table-layout: fixed; + width: 100%; } .mx_RoomDirectory_roomAvatar { - width: 32px; - padding-right: 14px; - vertical-align: top; -} - -.mx_RoomDirectory_roomDescription { - padding-bottom: 16px; + padding: 2px 14px 0 0; } .mx_RoomDirectory_roomMemberCount { + align-self: center; color: $light-fg-color; - width: 60px; - padding: 0 10px; - text-align: center; + padding: 3px 10px 0; &::before { background-color: $light-fg-color; @@ -105,8 +100,7 @@ limitations under the License. } .mx_RoomDirectory_join, .mx_RoomDirectory_preview { - width: 80px; - text-align: center; + align-self: center; white-space: nowrap; } diff --git a/res/css/structures/_RoomSearch.scss b/res/css/structures/_RoomSearch.scss index c33a3c0ff9..7fdafab5a6 100644 --- a/res/css/structures/_RoomSearch.scss +++ b/res/css/structures/_RoomSearch.scss @@ -22,7 +22,7 @@ limitations under the License. // keep border thickness consistent to prevent movement border: 1px solid transparent; height: 28px; - padding: 2px; + padding: 1px; // Create a flexbox for the icons (easier to manage) display: flex; diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss index 572c7166d2..26382b55e8 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -20,35 +20,54 @@ limitations under the License. flex-direction: column; } + +@keyframes mx_RoomView_fileDropTarget_animation { + from { + opacity: 0; + } + to { + opacity: 0.95; + } +} + .mx_RoomView_fileDropTarget { min-width: 0px; width: 100%; + height: 100%; + font-size: $font-18px; text-align: center; pointer-events: none; - padding-left: 12px; - padding-right: 12px; - margin-left: -12px; + background-color: $primary-bg-color; + opacity: 0.95; - border-top-left-radius: 10px; - border-top-right-radius: 10px; - - background-color: $droptarget-bg-color; - border: 2px #e1dddd solid; - border-bottom: none; position: absolute; - top: 52px; - bottom: 0px; z-index: 3000; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + animation: mx_RoomView_fileDropTarget_animation; + animation-duration: 0.5s; } -.mx_RoomView_fileDropTargetLabel { - top: 50%; - width: 100%; - margin-top: -50px; - position: absolute; +@keyframes mx_RoomView_fileDropTarget_image_animation { + from { + width: 0px; + } + to { + width: 32px; + } +} + +.mx_RoomView_fileDropTarget_image { + animation: mx_RoomView_fileDropTarget_image_animation; + animation-duration: 0.5s; + margin-bottom: 16px; } .mx_RoomView_auxPanel { @@ -117,7 +136,6 @@ limitations under the License. } .mx_RoomView_body { - position: relative; //for .mx_RoomView_auxPanel_fullHeight display: flex; flex-direction: column; flex: 1; @@ -219,7 +237,7 @@ hr.mx_RoomView_myReadMarker { position: relative; top: -1px; z-index: 1; - transition: width 400ms easeInSine 1s, opacity 400ms easeInSine 1s; + transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s; width: 99%; opacity: 1; } diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss new file mode 100644 index 0000000000..873fa967ab --- /dev/null +++ b/res/css/structures/_SpacePanel.scss @@ -0,0 +1,353 @@ +/* +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. +*/ + +$topLevelHeight: 32px; +$nestedHeight: 24px; +$gutterSize: 16px; +$activeBorderTransparentGap: 1px; + +$activeBackgroundColor: $roomtile-selected-bg-color; +$activeBorderColor: $secondary-fg-color; + +.mx_SpacePanel { + flex: 0 0 auto; + background-color: $groupFilterPanel-bg-color; + padding: 0; + margin: 0; + + // Create another flexbox so the Panel fills the container + display: flex; + flex-direction: column; + overflow-y: auto; + + .mx_SpacePanel_spaceTreeWrapper { + flex: 1; + overflow-y: scroll; + } + + .mx_SpacePanel_toggleCollapse { + flex: 0 0 auto; + width: 40px; + height: 40px; + mask-position: center; + mask-size: 32px; + mask-repeat: no-repeat; + margin-left: $gutterSize; + margin-bottom: 12px; + background-color: $roomlist-header-color; + mask-image: url('$(res)/img/element-icons/expand-space-panel.svg'); + + &.expanded { + transform: scaleX(-1); + } + } + + ul { + margin: 0; + list-style: none; + padding: 0; + padding-left: 16px; + } + + .mx_AutoHideScrollbar { + padding: 8px 0 16px; + } + + .mx_SpaceButton_toggleCollapse { + cursor: pointer; + } + + .mx_SpaceTreeLevel { + display: flex; + flex-direction: column; + max-width: 250px; + flex-grow: 1; + } + + .mx_SpaceItem { + display: inline-flex; + flex-flow: wrap; + } + + .mx_SpaceItem.collapsed { + & > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse { + transform: rotate(-90deg); + } + + & > .mx_SpaceTreeLevel { + display: none; + } + } + + .mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton { + margin-left: $gutterSize; + min-width: 40px; + } + + .mx_SpaceButton { + border-radius: 8px; + display: flex; + align-items: center; + padding: 4px 4px 4px 0; + width: 100%; + + &.mx_SpaceButton_active { + &:not(.mx_SpaceButton_narrow) .mx_SpaceButton_selectionWrapper { + background-color: $activeBackgroundColor; + } + + &.mx_SpaceButton_narrow .mx_SpaceButton_selectionWrapper { + padding: $activeBorderTransparentGap; + border: 3px $activeBorderColor solid; + } + } + + .mx_SpaceButton_selectionWrapper { + position: relative; + display: flex; + flex: 1; + align-items: center; + border-radius: 12px; + padding: 4px; + } + + &:not(.mx_SpaceButton_narrow) { + .mx_SpaceButton_selectionWrapper { + width: 100%; + padding-right: 16px; + overflow: hidden; + } + } + + .mx_SpaceButton_name { + flex: 1; + margin-left: 8px; + white-space: nowrap; + display: block; + text-overflow: ellipsis; + overflow: hidden; + padding-right: 8px; + font-size: $font-14px; + line-height: $font-18px; + } + + .mx_SpaceButton_toggleCollapse { + width: $gutterSize; + height: 20px; + mask-position: center; + mask-size: 20px; + mask-repeat: no-repeat; + background-color: $roomlist-header-color; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + .mx_SpaceButton_icon { + width: $topLevelHeight; + min-width: $topLevelHeight; + height: $topLevelHeight; + border-radius: 8px; + position: relative; + + &::before { + position: absolute; + content: ''; + width: $topLevelHeight; + height: $topLevelHeight; + top: 0; + left: 0; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 18px; + } + } + + &.mx_SpaceButton_home .mx_SpaceButton_icon { + background-color: #ffffff; + + &::before { + background-color: #3f3d3d; + mask-image: url('$(res)/img/element-icons/home.svg'); + } + } + + &.mx_SpaceButton_new .mx_SpaceButton_icon { + background-color: $accent-color; + transition: all .1s ease-in-out; // TODO transition + + &::before { + background-color: #ffffff; + mask-image: url('$(res)/img/element-icons/plus.svg'); + transition: all .2s ease-in-out; // TODO transition + } + } + + &.mx_SpaceButton_newCancel .mx_SpaceButton_icon { + background-color: $icon-button-color; + + &::before { + transform: rotate(45deg); + } + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + + .mx_SpaceButton_menuButton { + width: 20px; + min-width: 20px; // yay flex + height: 20px; + margin-top: auto; + margin-bottom: auto; + display: none; + position: absolute; + right: 4px; + + &::before { + top: 2px; + left: 2px; + content: ''; + width: 16px; + height: 16px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/context-menu.svg'); + background: $primary-fg-color; + } + } + } + + .mx_SpacePanel_badgeContainer { + position: absolute; + height: 16px; + + // Create a flexbox to make aligning dot badges easier + display: flex; + align-items: center; + + .mx_NotificationBadge { + margin: 0 2px; // centering + } + + .mx_NotificationBadge_dot { + // make the smaller dot occupy the same width for centering + margin-left: 7px; + margin-right: 7px; + } + } + + &.collapsed { + .mx_SpaceButton { + .mx_SpacePanel_badgeContainer { + right: -3px; + top: -3px; + } + + &.mx_SpaceButton_active .mx_SpacePanel_badgeContainer { + // when we draw the selection border we move the relative bounds of our parent + // so update our position within the bounds of the parent to maintain position overall + right: -6px; + top: -6px; + } + } + } + + &:not(.collapsed) { + .mx_SpacePanel_badgeContainer { + position: absolute; + right: 4px; + } + + .mx_SpaceButton:hover, + .mx_SpaceButton:focus-within, + .mx_SpaceButton_hasMenuOpen { + // Hide the badge container on hover because it'll be a menu button + .mx_SpacePanel_badgeContainer { + width: 0; + height: 0; + display: none; + } + + .mx_SpaceButton_menuButton { + display: block; + } + } + } + + /* root space buttons are bigger and not indented */ + & > .mx_AutoHideScrollbar { + & > .mx_SpaceButton { + height: $topLevelHeight; + + &.mx_SpaceButton_active::before { + height: $topLevelHeight; + } + } + + & > ul { + padding-left: 0; + } + } +} + +.mx_SpacePanel_contextMenu { + .mx_SpacePanel_contextMenu_header { + margin: 12px 16px 12px; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + } + + .mx_IconizedContextMenu_optionList .mx_AccessibleButton.mx_SpacePanel_contextMenu_inviteButton { + color: $accent-color; + + .mx_SpacePanel_iconInvite::before { + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + + .mx_SpacePanel_iconSettings::before { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + + .mx_SpacePanel_iconLeave::before { + mask-image: url('$(res)/img/element-icons/leave.svg'); + } + + .mx_SpacePanel_iconMembers::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_SpacePanel_iconPlus::before { + mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg'); + } + + .mx_SpacePanel_iconHash::before { + mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg'); + } + + .mx_SpacePanel_iconExplore::before { + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); + } +} + + +.mx_SpacePanel_sharePublicSpace { + margin: 0; +} diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss new file mode 100644 index 0000000000..dcceee6371 --- /dev/null +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -0,0 +1,303 @@ +/* +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_SpaceRoomDirectory_dialogWrapper > .mx_Dialog { + max-width: 960px; + height: 100%; +} + +.mx_SpaceRoomDirectory { + height: 100%; + margin-bottom: 12px; + color: $primary-fg-color; + word-break: break-word; + display: flex; + flex-direction: column; + + .mx_Dialog_title { + display: flex; + + .mx_BaseAvatar { + margin-right: 12px; + align-self: center; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + + > div { + > h1 { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + margin: 0; + } + + > div { + font-weight: 400; + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } + } + } + + .mx_Dialog_content { + .mx_AccessibleButton_kind_link { + padding: 0; + } + + .mx_SearchBox { + margin: 24px 0 16px; + } + + .mx_SpaceRoomDirectory_noResults { + text-align: center; + + > div { + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + } + } + + .mx_SpaceRoomDirectory_listHeader { + display: flex; + min-height: 32px; + align-items: center; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + + .mx_AccessibleButton { + padding: 2px 8px; + font-weight: normal; + + & + .mx_AccessibleButton { + margin-left: 16px; + } + } + + > span { + margin-left: auto; + } + } + + .mx_SpaceRoomDirectory_error { + position: relative; + font-weight: $font-semi-bold; + color: $notice-primary-color; + font-size: $font-15px; + line-height: $font-18px; + margin: 20px auto 12px; + padding-left: 24px; + width: max-content; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 0; + background-image: url("$(res)/img/element-icons/warning-badge.svg"); + } + } + } +} + +.mx_SpaceRoomDirectory_list { + margin-top: 16px; + padding-bottom: 40px; + + .mx_SpaceRoomDirectory_roomCount { + > h3 { + display: inline; + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + color: $primary-fg-color; + } + + > span { + margin-left: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + } + } + + .mx_SpaceRoomDirectory_subspace { + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_SpaceRoomDirectory_subspace_toggle { + position: absolute; + left: -1px; + top: 10px; + height: 16px; + width: 16px; + border-radius: 4px; + background-color: $primary-bg-color; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 16px; + width: 16px; + mask-repeat: no-repeat; + mask-position: center; + background-color: $tertiary-fg-color; + mask-size: 16px; + transform: rotate(270deg); + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + &.mx_SpaceRoomDirectory_subspace_toggle_shown::before { + transform: rotate(0deg); + } + } + + .mx_SpaceRoomDirectory_subspace_children { + position: relative; + padding-left: 12px; + } + + .mx_SpaceRoomDirectory_roomTile { + position: relative; + padding: 8px 16px; + border-radius: 8px; + min-height: 56px; + box-sizing: border-box; + + display: grid; + grid-template-columns: 20px auto max-content; + grid-column-gap: 8px; + grid-row-gap: 6px; + align-items: center; + + .mx_BaseAvatar { + grid-row: 1; + grid-column: 1; + } + + .mx_SpaceRoomDirectory_roomTile_name { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + grid-row: 1; + grid-column: 2; + + .mx_InfoTooltip { + display: inline; + margin-left: 12px; + color: $tertiary-fg-color; + font-size: $font-12px; + line-height: $font-15px; + + .mx_InfoTooltip_icon { + margin-right: 4px; + position: relative; + vertical-align: text-top; + + &::before { + position: absolute; + top: 0; + left: 0; + } + } + } + } + + .mx_SpaceRoomDirectory_roomTile_info { + font-size: $font-14px; + line-height: $font-18px; + color: $secondary-fg-color; + grid-row: 2; + grid-column: 1/3; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + } + + .mx_SpaceRoomDirectory_actions { + text-align: right; + margin-left: 20px; + grid-column: 3; + grid-row: 1/3; + + .mx_AccessibleButton { + padding: 8px 18px; + display: inline-block; + visibility: hidden; + } + + .mx_Checkbox { + display: inline-flex; + vertical-align: middle; + margin-left: 12px; + } + } + + &:hover { + background-color: $groupFilterPanel-bg-color; + + .mx_AccessibleButton { + visibility: visible; + } + } + } + + .mx_SpaceRoomDirectory_roomTile, + .mx_SpaceRoomDirectory_subspace_children { + &::before { + content: ""; + position: absolute; + background-color: $groupFilterPanel-bg-color; + width: 1px; + height: 100%; + left: 6px; + top: 0; + } + } + + .mx_SpaceRoomDirectory_actions { + .mx_SpaceRoomDirectory_actionsText { + font-weight: normal; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } + + > hr { + border: none; + height: 1px; + background-color: rgba(141, 151, 165, 0.2); + margin: 20px 0; + } + + .mx_SpaceRoomDirectory_createRoom { + display: block; + margin: 16px auto 0; + width: max-content; + } +} diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss new file mode 100644 index 0000000000..2e7cfb55d9 --- /dev/null +++ b/res/css/structures/_SpaceRoomView.scss @@ -0,0 +1,465 @@ +/* +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. +*/ + +$SpaceRoomViewInnerWidth: 428px; + +@define-mixin SpacePillButton { + position: relative; + padding: 16px 32px 16px 72px; + width: 432px; + box-sizing: border-box; + border-radius: 8px; + border: 1px solid $input-border-color; + font-size: $font-15px; + margin: 20px 0; + + > h3 { + font-weight: $font-semi-bold; + margin: 0 0 4px; + } + + > span { + color: $secondary-fg-color; + } + + &::before { + position: absolute; + content: ''; + width: 32px; + height: 32px; + top: 24px; + left: 20px; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 24px; + background-color: $tertiary-fg-color; + } + + &:hover { + border-color: $accent-color; + + &::before { + background-color: $accent-color; + } + + > span { + color: $primary-fg-color; + } + } +} + +.mx_SpaceRoomView { + .mx_MainSplit > div:first-child { + padding: 80px 60px; + flex-grow: 1; + max-height: 100%; + overflow-y: auto; + + h1 { + margin: 0; + font-size: $font-24px; + font-weight: $font-semi-bold; + color: $primary-fg-color; + width: max-content; + } + + .mx_SpaceRoomView_description { + font-size: $font-15px; + color: $secondary-fg-color; + margin-top: 12px; + margin-bottom: 24px; + } + + .mx_SpaceRoomView_buttons { + display: block; + margin-top: 44px; + width: $SpaceRoomViewInnerWidth; + text-align: right; // button alignment right + + .mx_AccessibleButton_hasKind { + padding: 8px 22px; + margin-left: 16px; + } + } + + .mx_Field { + max-width: $SpaceRoomViewInnerWidth; + + & + .mx_Field { + margin-top: 28px; + } + } + + .mx_SpaceRoomView_errorText { + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + color: $notice-primary-color; + margin-bottom: 28px; + } + + .mx_AccessibleButton_disabled { + cursor: not-allowed; + } + } + + .mx_SpaceRoomView_preview { + padding: 32px 24px !important; // override default padding from above + margin: auto; + max-width: 480px; + box-sizing: border-box; + box-shadow: 2px 15px 30px $dialog-shadow-color; + border-radius: 8px; + + .mx_SpaceRoomView_preview_inviter { + display: flex; + align-items: center; + margin-bottom: 20px; + font-size: $font-15px; + + > div { + margin-left: 8px; + + .mx_SpaceRoomView_preview_inviter_name { + line-height: $font-18px; + } + + .mx_SpaceRoomView_preview_inviter_mxid { + line-height: $font-24px; + color: $secondary-fg-color; + } + } + } + + > .mx_BaseAvatar_image, + > .mx_BaseAvatar > .mx_BaseAvatar_image { + border-radius: 12px; + } + + h1.mx_SpaceRoomView_preview_name { + margin: 20px 0 !important; // override default margin from above + } + + .mx_SpaceRoomView_preview_topic { + font-size: $font-14px; + line-height: $font-22px; + color: $secondary-fg-color; + margin: 20px 0; + max-height: 160px; + overflow-y: auto; + } + + .mx_SpaceRoomView_preview_joinButtons { + margin-top: 20px; + + .mx_AccessibleButton { + width: 200px; + box-sizing: border-box; + padding: 14px 0; + + & + .mx_AccessibleButton { + margin-left: 20px; + } + } + } + } + + .mx_SpaceRoomView_landing { + > .mx_BaseAvatar_image, + > .mx_BaseAvatar > .mx_BaseAvatar_image { + border-radius: 12px; + } + + .mx_SpaceRoomView_landing_name { + margin: 24px 0 16px; + font-size: $font-15px; + color: $secondary-fg-color; + + > span { + display: inline-block; + } + + .mx_SpaceRoomView_landing_nameRow { + margin-top: 12px; + + > h1 { + display: inline-block; + } + } + + .mx_SpaceRoomView_landing_inviter { + .mx_BaseAvatar { + margin-right: 4px; + vertical-align: middle; + } + } + } + + .mx_SpaceRoomView_landing_info { + display: flex; + align-items: center; + + .mx_SpaceRoomView_info { + display: inline-block; + margin: 0; + } + + .mx_FacePile { + display: inline-block; + margin-left: auto; + margin-right: 12px; + + .mx_FacePile_faces { + cursor: pointer; + + > span:hover { + .mx_BaseAvatar { + filter: brightness(0.8); + } + } + + > span:first-child { + position: relative; + + .mx_BaseAvatar { + filter: brightness(0.8); + } + + &::before { + content: ""; + z-index: 1; + position: absolute; + top: 0; + left: 0; + height: 30px; + width: 30px; + background: #ffffff; // white icon fill + mask-position: center; + mask-size: 24px; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + } + } + } + } + + .mx_SpaceRoomView_landing_inviteButton { + position: relative; + padding-left: 40px; + height: min-content; + + &::before { + position: absolute; + content: ""; + left: 8px; + height: 16px; + width: 16px; + background: #ffffff; // white icon fill + mask-position: center; + mask-size: 16px; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + } + + .mx_SpaceRoomView_landing_topic { + font-size: $font-15px; + margin-top: 12px; + margin-bottom: 16px; + } + + > hr { + border: none; + height: 1px; + background-color: $groupFilterPanel-bg-color; + } + + .mx_SpaceRoomView_landing_adminButtons { + margin-top: 24px; + + .mx_AccessibleButton { + position: relative; + width: 160px; + height: 124px; + box-sizing: border-box; + padding: 72px 16px 0; + border-radius: 12px; + border: 1px solid $input-border-color; + margin-right: 28px; + margin-bottom: 20px; + font-size: $font-14px; + display: inline-block; + vertical-align: bottom; + + &:last-child { + margin-right: 0; + } + + &:hover { + background-color: rgba(141, 151, 165, 0.1); + } + + &::before, &::after { + position: absolute; + content: ""; + left: 16px; + top: 16px; + height: 40px; + width: 40px; + border-radius: 20px; + } + + &::after { + mask-position: center; + mask-size: 30px; + mask-repeat: no-repeat; + background: #ffffff; // white icon fill + } + + &.mx_SpaceRoomView_landing_addButton { + &::before { + background-color: #ac3ba8; + } + + &::after { + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + } + } + + &.mx_SpaceRoomView_landing_createButton { + &::before { + background-color: #368bd6; + } + + &::after { + mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + } + } + + &.mx_SpaceRoomView_landing_settingsButton { + &::before { + background-color: #5c56f5; + } + + &::after { + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + } + } + } + + .mx_SearchBox { + margin: 0 0 20px; + } + } + + .mx_SpaceRoomView_privateScope { + .mx_AccessibleButton { + @mixin SpacePillButton; + } + + .mx_SpaceRoomView_privateScope_justMeButton::before { + mask-image: url('$(res)/img/element-icons/room/members.svg'); + } + + .mx_SpaceRoomView_privateScope_meAndMyTeammatesButton::before { + mask-image: url('$(res)/img/element-icons/community-members.svg'); + } + } + + .mx_SpaceRoomView_inviteTeammates { + .mx_SpaceRoomView_inviteTeammates_buttons { + color: $secondary-fg-color; + margin-top: 28px; + + .mx_AccessibleButton { + position: relative; + display: inline-block; + padding-left: 32px; + line-height: 24px; // to center icons + + &::before { + content: ""; + position: absolute; + height: 24px; + width: 24px; + top: 0; + left: 0; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + & + .mx_AccessibleButton { + margin-left: 32px; + } + } + + .mx_SpaceRoomView_inviteTeammates_inviteDialogButton::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + } +} + +.mx_SpaceRoomView_info { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + margin: 20px 0; + + .mx_SpaceRoomView_info_public, + .mx_SpaceRoomView_info_private { + padding-left: 20px; + position: relative; + + &::before { + position: absolute; + content: ""; + width: 20px; + height: 20px; + top: 0; + left: -2px; + mask-position: center; + mask-repeat: no-repeat; + background-color: $tertiary-fg-color; + } + } + + .mx_SpaceRoomView_info_public::before { + mask-size: 12px; + mask-image: url("$(res)/img/globe.svg"); + } + + .mx_SpaceRoomView_info_private::before { + mask-size: 14px; + mask-image: url("$(res)/img/element-icons/lock.svg"); + } + + .mx_AccessibleButton_kind_link { + color: inherit; + position: relative; + padding-left: 16px; + + &::before { + content: "·"; // visual separator + position: absolute; + left: 6px; + } + } +} diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index c381668a6a..09f834a6e3 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019-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. @@ -158,6 +158,10 @@ limitations under the License. } } + .mx_Toast_detail { + color: $secondary-fg-color; + } + .mx_Toast_deviceID { font-size: $font-10px; } diff --git a/res/css/structures/_UploadBar.scss b/res/css/structures/_UploadBar.scss index d76c81668c..7c62516b47 100644 --- a/res/css/structures/_UploadBar.scss +++ b/res/css/structures/_UploadBar.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015, 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. @@ -15,47 +15,45 @@ limitations under the License. */ .mx_UploadBar { + padding-left: 65px; // line up with the shield area in the composer position: relative; + + .mx_ProgressBar { + width: calc(100% - 40px); // cheating at a right margin + } } -.mx_UploadBar_uploadProgressOuter { - height: 5px; - margin-left: 63px; - margin-top: -1px; - padding-bottom: 5px; -} - -.mx_UploadBar_uploadProgressInner { - background-color: $accent-color; - height: 5px; -} - -.mx_UploadBar_uploadFilename { +.mx_UploadBar_filename { margin-top: 5px; - margin-left: 65px; - opacity: 0.5; - color: $primary-fg-color; -} - -.mx_UploadBar_uploadIcon { - float: left; - margin-top: 5px; - margin-left: 14px; -} - -.mx_UploadBar_uploadCancel { - float: right; - margin-top: 5px; - margin-right: 10px; + color: $muted-fg-color; position: relative; - opacity: 0.6; - cursor: pointer; - z-index: 1; + padding-left: 22px; // 18px for icon, 4px for padding + font-size: $font-15px; + vertical-align: middle; + + &::before { + content: ""; + height: 18px; + width: 18px; + position: absolute; + top: 0; + left: 0; + mask-repeat: no-repeat; + mask-position: center; + background-color: $muted-fg-color; + mask-image: url('$(res)/img/element-icons/upload.svg'); + } } -.mx_UploadBar_uploadBytes { - float: right; - margin-top: 5px; - margin-right: 30px; - color: $accent-color; +.mx_UploadBar_cancel { + position: absolute; + top: 0; + right: 0; + height: 16px; + width: 16px; + margin-right: 16px; // align over rightmost button in composer + mask-repeat: no-repeat; + mask-position: center; + background-color: $muted-fg-color; + mask-image: url('$(res)/img/icons-close.svg'); } diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 84c21364ce..3badb0850c 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -72,6 +72,7 @@ limitations under the License. position: relative; // to make default avatars work margin-right: 8px; height: 32px; // to remove the unknown 4px gap the browser puts below it + padding: 3px 0; // to align with and without using doubleName .mx_UserMenu_userAvatar { border-radius: 32px; // should match avatar size @@ -119,20 +120,16 @@ limitations under the License. } &.mx_UserMenu_minimized { - .mx_UserMenu_userHeader { - .mx_UserMenu_row { - justify-content: center; - } + padding-right: 0px; - .mx_UserMenu_userAvatarContainer { - margin-right: 0; - } + .mx_UserMenu_userAvatarContainer { + margin-right: 0px; } } } .mx_UserMenu_contextMenu { - width: 247px; + width: 258px; // These override the styles already present on the user menu rather than try to // define a new menu. They are specifically for the stacked menu when a community @@ -276,6 +273,9 @@ limitations under the License. .mx_UserMenu_iconHome::before { mask-image: url('$(res)/img/element-icons/roomlist/home.svg'); } + .mx_UserMenu_iconHosting::before { + mask-image: url('$(res)/img/element-icons/brands/element.svg'); + } .mx_UserMenu_iconBell::before { mask-image: url('$(res)/img/element-icons/notifications.svg'); diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss index 421d1f03cd..248eab5d88 100644 --- a/res/css/structures/_ViewSource.scss +++ b/res/css/structures/_ViewSource.scss @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ViewSource_label_left { - float: left; -} - -.mx_ViewSource_label_right { - float: right; -} - -.mx_ViewSource_label_bottom { +.mx_ViewSource_separator { clear: both; border-bottom: 1px solid #e5e5e5; + padding-top: 0.7em; + padding-bottom: 0.7em; +} + +.mx_ViewSource_heading { + font-size: $font-17px; + font-weight: 400; + color: $primary-fg-color; + margin-top: 0.7em; } .mx_ViewSource pre { @@ -34,3 +35,7 @@ limitations under the License. word-wrap: break-word; white-space: pre-wrap; } + +.mx_ViewSource_details { + margin-top: 0.8em; +} diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss index f742be70e4..80e7aaada0 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -26,50 +26,6 @@ limitations under the License. position: relative; } -.mx_CompleteSecurity_clients { - width: max-content; - margin: 36px auto 0; - - .mx_CompleteSecurity_clients_desktop, .mx_CompleteSecurity_clients_mobile { - position: relative; - width: 160px; - text-align: center; - padding-top: 64px; - display: inline-block; - - &::before { - content: ''; - position: absolute; - height: 48px; - width: 48px; - left: 56px; - top: 0; - background-color: $muted-fg-color; - mask-repeat: no-repeat; - mask-size: contain; - } - } - - .mx_CompleteSecurity_clients_desktop { - margin-right: 56px; - } - - .mx_CompleteSecurity_clients_desktop::before { - mask-image: url('$(res)/img/feather-customised/monitor.svg'); - } - - .mx_CompleteSecurity_clients_mobile::before { - mask-image: url('$(res)/img/feather-customised/smartphone.svg'); - } - - p { - margin-top: 16px; - font-size: $font-12px; - color: $muted-fg-color; - text-align: center; - } -} - .mx_CompleteSecurity_heroIcon { width: 128px; height: 128px; diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 8f0c758e7a..90dca32e48 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -34,7 +34,7 @@ limitations under the License. h3 { font-size: $font-14px; font-weight: 600; - color: $authpage-primary-color; + color: $authpage-secondary-color; } h3.mx_AuthBody_centered { diff --git a/res/css/views/auth/_AuthHeader.scss b/res/css/views/auth/_AuthHeader.scss index b1372affee..13d5195160 100644 --- a/res/css/views/auth/_AuthHeader.scss +++ b/res/css/views/auth/_AuthHeader.scss @@ -18,7 +18,7 @@ limitations under the License. display: flex; flex-direction: column; width: 206px; - padding: 25px 40px; + padding: 25px 25px; box-sizing: border-box; } diff --git a/res/css/views/auth/_AuthHeaderLogo.scss b/res/css/views/auth/_AuthHeaderLogo.scss index 917dcabf67..86f0313b68 100644 --- a/res/css/views/auth/_AuthHeaderLogo.scss +++ b/res/css/views/auth/_AuthHeaderLogo.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_AuthHeaderLogo { margin-top: 15px; flex: 1; - padding: 0 10px; + padding: 0 25px; } .mx_AuthHeaderLogo img { diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index 5df73b139f..ffaad3cd7a 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -83,7 +83,10 @@ limitations under the License. } .mx_InteractiveAuthEntryComponents_termsPolicy { - display: block; + display: flex; + flex-direction: row; + justify-content: start; + align-items: center; } .mx_InteractiveAuthEntryComponents_passwordSection { diff --git a/res/css/views/auth/_LanguageSelector.scss b/res/css/views/auth/_LanguageSelector.scss index 781561f876..885ee7f30d 100644 --- a/res/css/views/auth/_LanguageSelector.scss +++ b/res/css/views/auth/_LanguageSelector.scss @@ -23,6 +23,7 @@ limitations under the License. font-size: $font-14px; font-weight: 600; color: $authpage-lang-color; + width: auto; } .mx_AuthBody_language .mx_Dropdown_arrow { diff --git a/res/css/views/auth/_Welcome.scss b/res/css/views/auth/_Welcome.scss index f0e2b3de33..894174d6e2 100644 --- a/res/css/views/auth/_Welcome.scss +++ b/res/css/views/auth/_Welcome.scss @@ -18,7 +18,6 @@ limitations under the License. display: flex; flex-direction: column; align-items: center; - &.mx_WelcomePage_registrationDisabled { .mx_ButtonCreateAccount { display: none; @@ -27,6 +26,6 @@ limitations under the License. } .mx_Welcome .mx_AuthBody_language { - width: 120px; + width: 160px; margin-bottom: 10px; } diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss index e0afd9de66..2631cbfb40 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.scss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// XXX: We shouldn't be using TemporaryTile anywhere - delete it. -.mx_DecoratedRoomAvatar, .mx_TemporaryTile { +.mx_DecoratedRoomAvatar, .mx_ExtraTile { position: relative; &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss index d911ac6dfe..204435995f 100644 --- a/res/css/views/context_menus/_IconizedContextMenu.scss +++ b/res/css/views/context_menus/_IconizedContextMenu.scss @@ -75,6 +75,11 @@ limitations under the License. background-color: $menu-selected-color; } + &.mx_AccessibleButton_disabled { + opacity: 0.5; + cursor: not-allowed; + } + img, .mx_IconizedContextMenu_icon { // icons width: 16px; min-width: 16px; diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss new file mode 100644 index 0000000000..a7cfd7bde6 --- /dev/null +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -0,0 +1,196 @@ +/* +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_AddExistingToSpaceDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_AddExistingToSpaceDialog { + width: 480px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 80vh; + + .mx_Dialog_title { + display: flex; + + .mx_BaseAvatar_image { + border-radius: 8px; + margin: 0; + vertical-align: unset; + } + + .mx_BaseAvatar { + display: inline-flex; + margin: 5px 16px 5px 5px; + vertical-align: middle; + } + + > div { + > h1 { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + margin: 0; + } + + .mx_AddExistingToSpaceDialog_onlySpace { + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + } + } + + .mx_Dropdown_input { + border: none; + + > .mx_Dropdown_option { + padding-left: 0; + flex: unset; + height: unset; + color: $secondary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + + .mx_BaseAvatar { + display: none; + } + } + + .mx_Dropdown_menu { + .mx_AddExistingToSpaceDialog_dropdownOptionActive { + color: $accent-color; + padding-right: 32px; + position: relative; + + &::before { + content: ''; + width: 20px; + height: 20px; + top: 8px; + right: 0; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $accent-color; + mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg'); + } + } + } + } + } + + .mx_SearchBox { + margin: 0; + flex-grow: 0; + } + + .mx_AddExistingToSpaceDialog_errorText { + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + color: $notice-primary-color; + margin-bottom: 28px; + } + + .mx_AddExistingToSpaceDialog_content { + flex-grow: 1; + + .mx_AddExistingToSpaceDialog_noResults { + display: block; + margin-top: 24px; + } + } + + .mx_AddExistingToSpaceDialog_section { + margin-top: 24px; + + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_AddExistingToSpaceDialog_entry { + display: flex; + margin-top: 12px; + + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_AddExistingToSpaceDialog_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + } + + .mx_FormButton { + min-width: 92px; + font-weight: normal; + box-sizing: border-box; + } + } + } + + .mx_AddExistingToSpaceDialog_section_spaces { + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_AddExistingToSpaceDialog_footer { + display: flex; + margin-top: 32px; + + > span { + flex-grow: 1; + font-size: $font-14px; + line-height: $font-15px; + font-weight: $font-semi-bold; + + .mx_AccessibleButton { + font-size: inherit; + display: inline-block; + } + + > * { + vertical-align: middle; + } + } + + .mx_AccessibleButton { + display: inline-block; + } + + .mx_AccessibleButton_kind_link { + padding: 0; + } + } + + .mx_FormButton { + padding: 8px 22px; + } +} diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 35cb6bc7ab..8fee740016 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -223,3 +223,54 @@ limitations under the License. content: ":"; } } + +.mx_DevTools_SettingsExplorer { + table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; + + th { + // Colour choice: first one autocomplete gave me. + border-bottom: 1px solid $accent-color; + text-align: left; + } + + td, th { + width: 360px; // "feels right" number + + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + td + td, th + th { + width: auto; + } + + tr:hover { + // Colour choice: first one autocomplete gave me. + background-color: $accent-color-50pct; + } + } + + .mx_DevTools_SettingsExplorer_mutable { + background-color: $accent-color; + } + + .mx_DevTools_SettingsExplorer_immutable { + background-color: $warning-color; + } + + .mx_DevTools_SettingsExplorer_edit { + float: right; + margin-right: 16px; + } + + .mx_DevTools_SettingsExplorer_warning { + border: 2px solid $warning-color; + border-radius: 4px; + padding: 4px; + margin-bottom: 8px; + } +} diff --git a/res/css/views/dialogs/_HostSignupDialog.scss b/res/css/views/dialogs/_HostSignupDialog.scss new file mode 100644 index 0000000000..ac4bc41951 --- /dev/null +++ b/res/css/views/dialogs/_HostSignupDialog.scss @@ -0,0 +1,143 @@ +/* +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_HostSignupDialog { + width: 90vw; + max-width: 580px; + height: 80vh; + max-height: 600px; + // Ensure dialog borders are always white as the HostSignupDialog + // does not yet support dark mode or theming in general. + // In the future we might want to pass the theme to the called + // iframe, should some hosting provider have that need. + background-color: #ffffff; + + .mx_HostSignupDialog_info { + text-align: center; + + .mx_HostSignupDialog_content_top { + margin-bottom: 24px; + } + + .mx_HostSignupDialog_paragraphs { + text-align: left; + padding-left: 25%; + padding-right: 25%; + } + + .mx_HostSignupDialog_buttons { + margin-bottom: 24px; + display: flex; + justify-content: center; + + button { + padding: 12px; + margin: 0 16px; + } + } + + .mx_HostSignupDialog_footer { + display: flex; + justify-content: center; + align-items: baseline; + + img { + padding-right: 5px; + } + } + } + + iframe { + width: 100%; + height: 100%; + border: none; + background-color: #fff; + min-height: 540px; + } +} + +.mx_HostSignupDialog_text_dark { + color: $primary-fg-color; +} + +.mx_HostSignupDialog_text_light { + color: $secondary-fg-color; +} + +.mx_HostSignup_maximize_button { + mask: url('$(res)/img/feather-customised/maximise.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $dialog-close-fg-color; + cursor: pointer; + position: absolute; + top: 10px; + right: 10px; +} + +.mx_HostSignup_minimize_button { + mask: url('$(res)/img/feather-customised/minimise.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $dialog-close-fg-color; + cursor: pointer; + position: absolute; + top: 10px; + right: 25px; +} + +.mx_HostSignup_persisted { + width: 90vw; + max-width: 580px; + height: 80vh; + max-height: 600px; + top: 0; + left: 0; + position: fixed; + display: none; +} + +.mx_HostSignupDialog_minimized { + position: fixed; + bottom: 80px; + right: 26px; + width: 314px; + height: 217px; + overflow: hidden; + + &.mx_Dialog { + padding: 12px; + } + + .mx_Dialog_title { + text-align: left !important; + padding-left: 20px; + font-size: $font-15px; + } + + iframe { + width: 100%; + height: 100%; + border: none; + background-color: #fff; + } +} diff --git a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss index a1793cc75e..c97a3b69b7 100644 --- a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss +++ b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss @@ -89,24 +89,18 @@ limitations under the License. } } - .mx_showMore { - display: block; - text-align: left; - margin-top: 10px; - } - .metadata { color: $muted-fg-color; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; margin-bottom: 0; - } - - .metadata.visible { overflow-y: visible; text-overflow: ellipsis; white-space: normal; + padding: 0; + + > li { + padding: 0; + border: 0; + } } } } diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss new file mode 100644 index 0000000000..6e5fd9c8c8 --- /dev/null +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -0,0 +1,55 @@ +/* +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_SpaceSettingsDialog { + width: 480px; + color: $primary-fg-color; + + .mx_SpaceSettings_errorText { + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + color: $notice-primary-color; + margin-bottom: 28px; + } + + .mx_ToggleSwitch { + display: inline-block; + vertical-align: middle; + margin-left: 16px; + } + + .mx_AccessibleButton_kind_danger { + margin-top: 28px; + } + + .mx_SpaceSettingsDialog_buttons { + display: flex; + margin-top: 64px; + + .mx_AccessibleButton { + display: inline-block; + } + + .mx_AccessibleButton_kind_link { + margin-left: auto; + } + } + + .mx_AccessibleButton_hasKind { + padding: 8px 22px; + } +} diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 9c26f8f120..0075dcb511 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -26,7 +26,9 @@ limitations under the License. padding: 7px 18px; text-align: center; border-radius: 8px; - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; font-size: $font-14px; } diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss new file mode 100644 index 0000000000..69dde5925e --- /dev/null +++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss @@ -0,0 +1,72 @@ +/* +Copyright 2021 Šimon Brandner + +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_desktopCapturerSourcePicker { + overflow: hidden; +} + +.mx_desktopCapturerSourcePicker_tabLabels { + display: flex; + padding: 0 0 8px 0; +} + +.mx_desktopCapturerSourcePicker_tabLabel, +.mx_desktopCapturerSourcePicker_tabLabel_selected { + width: 100%; + text-align: center; + border-radius: 8px; + padding: 8px 0; + font-size: $font-13px; +} + +.mx_desktopCapturerSourcePicker_tabLabel_selected { + background-color: $tab-label-active-bg-color; + color: $tab-label-active-fg-color; +} + +.mx_desktopCapturerSourcePicker_panel { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + height: 500px; + overflow: overlay; +} + +.mx_desktopCapturerSourcePicker_stream_button { + display: flex; + flex-direction: column; + margin: 8px; + border-radius: 4px; +} + +.mx_desktopCapturerSourcePicker_stream_button:hover, +.mx_desktopCapturerSourcePicker_stream_button:focus { + background: $roomtile-selected-bg-color; +} + +.mx_desktopCapturerSourcePicker_stream_thumbnail { + margin: 4px; + width: 312px; +} + +.mx_desktopCapturerSourcePicker_stream_name { + margin: 0 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 312px; +} diff --git a/res/css/views/elements/_FacePile.scss b/res/css/views/elements/_FacePile.scss new file mode 100644 index 0000000000..9a992f59d1 --- /dev/null +++ b/res/css/views/elements/_FacePile.scss @@ -0,0 +1,42 @@ +/* +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_FacePile { + .mx_FacePile_faces { + display: inline-flex; + flex-direction: row-reverse; + vertical-align: middle; + + > span + span { + margin-right: -8px; + } + + .mx_BaseAvatar_image { + border: 1px solid $primary-bg-color; + } + + .mx_BaseAvatar_initial { + margin: 1px; // to offset the border on the image + } + } + + > span { + margin-left: 12px; + font-size: $font-14px; + line-height: $font-24px; + color: $tertiary-fg-color; + } +} diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss index 7ec01f17e6..eda201ff03 100644 --- a/res/css/views/elements/_FormButton.scss +++ b/res/css/views/elements/_FormButton.scss @@ -33,4 +33,10 @@ limitations under the License. color: $notice-primary-color; background-color: $notice-primary-bg-color; } + + &.mx_AccessibleButton_kind_secondary { + color: $secondary-fg-color; + border: 1px solid $secondary-fg-color; + background-color: unset; + } } diff --git a/res/css/views/elements/_MiniAvatarUploader.scss b/res/css/views/elements/_MiniAvatarUploader.scss index 2502977331..698184a095 100644 --- a/res/css/views/elements/_MiniAvatarUploader.scss +++ b/res/css/views/elements/_MiniAvatarUploader.scss @@ -18,6 +18,16 @@ limitations under the License. position: relative; width: min-content; + // this isn't a floating tooltip so override some things to not need to bother with z-index and floating + .mx_Tooltip { + display: inline-block; + position: absolute; + z-index: unset; + width: max-content; + left: 72px; + top: 0; + } + &::before, &::after { content: ''; position: absolute; diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss index e49d85af04..770978e921 100644 --- a/res/css/views/elements/_ProgressBar.scss +++ b/res/css/views/elements/_ProgressBar.scss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020, 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. @@ -15,15 +15,15 @@ limitations under the License. */ progress.mx_ProgressBar { - height: 4px; + height: 6px; width: 60px; - border-radius: 10px; overflow: hidden; appearance: none; - border: 0; + border: none; - @mixin ProgressBarBorderRadius "10px"; - @mixin ProgressBarColour $accent-color; + @mixin ProgressBarBorderRadius "6px"; + @mixin ProgressBarColour $progressbar-fg-color; + @mixin ProgressBarBgColour $progressbar-bg-color; ::-webkit-progress-value { transition: width 1s; } diff --git a/res/css/views/elements/_SSOButtons.scss b/res/css/views/elements/_SSOButtons.scss index f762468c7f..e02816780f 100644 --- a/res/css/views/elements/_SSOButtons.scss +++ b/res/css/views/elements/_SSOButtons.scss @@ -16,13 +16,26 @@ limitations under the License. .mx_SSOButtons { display: flex; + flex-wrap: wrap; justify-content: center; + .mx_SSOButtons_row { + & + .mx_SSOButtons_row { + margin-top: 16px; + } + } + .mx_SSOButton { position: relative; width: 100%; - padding-left: 32px; - padding-right: 32px; + padding: 7px 32px; + text-align: center; + border-radius: 8px; + display: inline-block; + font-size: $font-14px; + font-weight: $font-semi-bold; + border: 1px solid $input-border-color; + color: $primary-fg-color; > img { object-fit: contain; @@ -32,10 +45,22 @@ limitations under the License. } } + .mx_SSOButton_default { + color: $button-primary-bg-color; + background-color: $button-secondary-bg-color; + border-color: $button-primary-bg-color; + } + .mx_SSOButton_default.mx_SSOButton_primary { + color: $button-primary-fg-color; + background-color: $button-primary-bg-color; + } + .mx_SSOButton_mini { box-sizing: border-box; width: 50px; // 48px + 1px border on all sides height: 50px; // 48px + 1px border on all sides + min-width: 50px; // prevent crushing by the flexbox + padding: 12px; > img { left: 12px; @@ -43,7 +68,7 @@ limitations under the License. } & + .mx_SSOButton_mini { - margin-left: 24px; + margin-left: 16px; } } } diff --git a/res/css/views/elements/_ServerPicker.scss b/res/css/views/elements/_ServerPicker.scss index ae1e445a9f..188eb5d655 100644 --- a/res/css/views/elements/_ServerPicker.scss +++ b/res/css/views/elements/_ServerPicker.scss @@ -59,7 +59,7 @@ limitations under the License. } .mx_ServerPicker_server { - color: $primary-fg-color; + color: $authpage-primary-color; grid-column: 1; grid-row: 2; margin-bottom: 16px; diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index 6cbce68745..b45126acf8 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015, 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,6 +16,19 @@ limitations under the License. .mx_MFileBody_download { color: $accent-color; + + .mx_MFileBody_download_icon { + // 12px instead of 14px to better match surrounding font size + width: 12px; + height: 12px; + mask-size: 12px; + + mask-position: center; + mask-repeat: no-repeat; + mask-image: url("$(res)/img/download.svg"); + background-color: $accent-color; + display: inline-block; + } } .mx_MFileBody_download a { @@ -45,3 +58,46 @@ limitations under the License. * big the content of the iframe is. */ height: 1.5em; } + +.mx_MFileBody_info { + background-color: $message-body-panel-bg-color; + border-radius: 4px; + width: 270px; + padding: 8px; + color: $message-body-panel-fg-color; + + .mx_MFileBody_info_icon { + background-color: $message-body-panel-icon-bg-color; + border-radius: 20px; + display: inline-block; + width: 32px; + height: 32px; + position: relative; + vertical-align: middle; + margin-right: 12px; + + &::before { + content: ''; + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); + background-color: $message-body-panel-fg-color; + width: 13px; + height: 15px; + + position: absolute; + top: 8px; + left: 9px; + } + } + + .mx_MFileBody_info_filename { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: inline-block; + width: calc(100% - 32px - 12px); // 32px icon, 12px margin on the icon + vertical-align: middle; + } +} diff --git a/res/css/views/messages/_RedactedBody.scss b/res/css/views/messages/_RedactedBody.scss index e4ab0c0835..600ac0c6b7 100644 --- a/res/css/views/messages/_RedactedBody.scss +++ b/res/css/views/messages/_RedactedBody.scss @@ -30,7 +30,7 @@ limitations under the License. mask-size: contain; content: ''; position: absolute; - top: 2px; + top: 1px; left: 0; } } diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss index 076932ee97..66825030e0 100644 --- a/res/css/views/messages/_ViewSourceEvent.scss +++ b/res/css/views/messages/_ViewSourceEvent.scss @@ -35,13 +35,13 @@ limitations under the License. mask-size: auto 12px; visibility: hidden; background-color: $accent-color; - mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); + mask-image: url('$(res)/img/feather-customised/maximise.svg'); } &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { mask-position: 0 bottom; margin-bottom: 7px; - mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); + mask-image: url('$(res)/img/feather-customised/minimise.svg'); } &:hover .mx_ViewSourceEvent_toggle { diff --git a/res/css/views/right_panel/_EncryptionInfo.scss b/res/css/views/right_panel/_EncryptionInfo.scss index e13b1b6802..b3d4275f60 100644 --- a/res/css/views/right_panel/_EncryptionInfo.scss +++ b/res/css/views/right_panel/_EncryptionInfo.scss @@ -14,13 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserInfo { - .mx_EncryptionInfo_spinner { - .mx_Spinner { - margin-top: 25px; - margin-bottom: 15px; - } - - text-align: center; +.mx_EncryptionInfo_spinner { + .mx_Spinner { + margin-top: 25px; + margin-bottom: 15px; } + + text-align: center; } diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 8731d22660..fd80836237 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -24,26 +24,45 @@ $MiniAppTileHeight: 200px; flex-direction: column; overflow: hidden; + .mx_AppsContainer_resizerHandleContainer { + width: 100%; + height: 10px; + margin-top: -3px; // move it up so the interactions are slightly more comfortable + display: block; + position: relative; + } + .mx_AppsContainer_resizerHandle { cursor: ns-resize; - border-radius: 3px; - // Override styles from library - width: unset !important; - height: 4px !important; + // Override styles from library, making the whole area the target area + width: 100% !important; + height: 100% !important; // This is positioned directly below frame position: absolute; - bottom: -8px !important; // override from library + bottom: 0 !important; // override from library - // Together, these make the bar 64px wide - // These are also overridden from the library - left: calc(50% - 32px) !important; - right: calc(50% - 32px) !important; + // We then render the pill handle in an ::after to keep it in the handle's + // area without being a massive line across the screen + &::after { + content: ''; + position: absolute; + border-radius: 3px; + + // The combination of these two should make the pill 4px high + top: 6px; + bottom: 0; + + // Together, these make the bar 64px wide + // These are also overridden from the library + left: calc(50% - 32px); + right: calc(50% - 32px); + } } &:hover { - .mx_AppsContainer_resizerHandle { + .mx_AppsContainer_resizerHandle::after { opacity: 0.8; background: $primary-fg-color; } @@ -351,11 +370,6 @@ $MinWidth: 240px; display: none; } -/* Avoid apptile iframes capturing mouse event focus when resizing */ -.mx_AppsDrawer_resizing iframe { - pointer-events: none; -} - .mx_AppsDrawer_resizing .mx_AppTile_persistedWrapper { z-index: 1; } diff --git a/res/css/views/rooms/_AuxPanel.scss b/res/css/views/rooms/_AuxPanel.scss index 34ef5e01d4..17a6294bf0 100644 --- a/res/css/views/rooms/_AuxPanel.scss +++ b/res/css/views/rooms/_AuxPanel.scss @@ -17,7 +17,7 @@ limitations under the License. .m_RoomView_auxPanel_stateViews { padding: 5px; padding-left: 19px; - border-bottom: 1px solid #e5e5e5; + border-bottom: 1px solid $primary-hairline-color; } .m_RoomView_auxPanel_stateViews_span a { diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e126e523a6..4f58c08617 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -66,6 +66,11 @@ limitations under the License. } } } + + &.mx_BasicMessageComposer_input_disabled { + pointer-events: none; + cursor: not-allowed; + } } .mx_BasicMessageComposer_AutoCompleteWrapper { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 429ac7ed4b..028d9a7556 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -26,7 +26,7 @@ $left-gutter: 64px; } .mx_EventTile.mx_EventTile_info { - padding-top: 0px; + padding-top: 1px; } .mx_EventTile_avatar { @@ -37,7 +37,7 @@ $left-gutter: 64px; } .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { - top: $font-8px; + top: $font-6px; left: $left-gutter; } @@ -74,7 +74,6 @@ $left-gutter: 64px; margin-left: 5px; display: inline-block; vertical-align: top; - height: 16px; overflow: hidden; user-select: none; @@ -214,23 +213,36 @@ $left-gutter: 64px; color: $accent-fg-color; } -.mx_EventTile_encrypting { - color: $event-encrypting-color !important; -} - -.mx_EventTile_sending { - color: $event-sending-color; -} - -.mx_EventTile_sending .mx_UserPill, -.mx_EventTile_sending .mx_RoomPill { - opacity: 0.5; -} - .mx_EventTile_notSent { color: $event-notsent-color; } +.mx_EventTile_receiptSent, +.mx_EventTile_receiptSending { + // We don't use `position: relative` on the element because then it won't line + // up with the other read receipts + + &::before { + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 14px; + width: 14px; + height: 14px; + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + } +} +.mx_EventTile_receiptSent::before { + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); +} +.mx_EventTile_receiptSending::before { + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); +} + .mx_EventTile_contextual { opacity: 0.4; } @@ -258,17 +270,13 @@ $left-gutter: 64px; display: inline-block; width: 14px; height: 14px; - top: 29px; + // This aligns the avatar with the last line of the + // message. We want to move it one line up - 2.2rem + top: -2.2rem; user-select: none; z-index: 1; } -.mx_EventTile_continuation .mx_EventTile_readAvatars, -.mx_EventTile_info .mx_EventTile_readAvatars, -.mx_EventTile_emote .mx_EventTile_readAvatars { - top: 7px; -} - .mx_EventTile_readAvatars .mx_BaseAvatar { position: absolute; display: inline-block; @@ -421,15 +429,15 @@ $left-gutter: 64px; } .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { - border-left: $e2e-verified-color 4px solid; + border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; } .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { - border-left: $e2e-unverified-color 4px solid; + border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; } .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - border-left: $e2e-unknown-color 4px solid; + border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; } .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, @@ -447,8 +455,7 @@ $left-gutter: 64px; .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { - left: 3px; - width: auto; + width: $MessageTimestamp_width_hover; } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) @@ -493,7 +500,6 @@ $left-gutter: 64px; // https://github.com/vector-im/vector-web/issues/754 overflow-x: overlay; overflow-y: visible; - max-height: 30vh; } code { @@ -502,6 +508,22 @@ $left-gutter: 64px; } } +.mx_EventTile_lineNumbers { + float: left; + margin: 0 0.5em 0 -1.5em; + color: gray; +} + +.mx_EventTile_lineNumber { + text-align: right; + display: block; + padding-left: 1em; +} + +.mx_EventTile_collapsedCodeBlock { + max-height: 30vh; +} + .mx_EventTile:hover .mx_EventTile_body pre, .mx_EventTile.focus-visible:focus-within .mx_EventTile_body pre { border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter @@ -513,21 +535,42 @@ $left-gutter: 64px; } // Inserted adjacent to
 blocks, (See TextualBody)
-.mx_EventTile_copyButton {
+.mx_EventTile_button {
     position: absolute;
     display: inline-block;
     visibility: hidden;
     cursor: pointer;
-    top: 6px;
-    right: 6px;
+    top: 8px;
+    right: 8px;
     width: 19px;
     height: 19px;
-    mask-image: url($copy-button-url);
     background-color: $message-action-bar-fg-color;
 }
+.mx_EventTile_buttonBottom {
+    top: 33px;
+}
+.mx_EventTile_copyButton {
+    mask-image: url($copy-button-url);
+}
+.mx_EventTile_collapseButton {
+    mask-size: 75%;
+    mask-position: center;
+    mask-repeat: no-repeat;
+    mask-image: url($collapse-button-url);
+}
+.mx_EventTile_expandButton {
+    mask-size: 75%;
+    mask-position: center;
+    mask-repeat: no-repeat;
+    mask-image: url($expand-button-url);
+}
 
 .mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton,
-.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton {
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton,
+.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_collapseButton,
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_collapseButton,
+.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_expandButton,
+.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_expandButton {
     visibility: visible;
 }
 
diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss
index 2b447be44a..818509785b 100644
--- a/res/css/views/rooms/_GroupLayout.scss
+++ b/res/css/views/rooms/_GroupLayout.scss
@@ -20,8 +20,8 @@ $left-gutter: 64px;
 .mx_GroupLayout {
     .mx_EventTile {
         > .mx_SenderProfile {
-            line-height: $font-17px;
-            padding-left: $left-gutter;
+            line-height: $font-20px;
+            margin-left: $left-gutter;
         }
 
         > .mx_EventTile_line {
@@ -34,11 +34,11 @@ $left-gutter: 64px;
 
         .mx_MessageTimestamp {
             position: absolute;
-            width: 46px; /* 8 + 30 (avatar) + 8 */
+            width: $MessageTimestamp_width;
         }
 
         .mx_EventTile_line, .mx_EventTile_reply {
-            padding-top: 3px;
+            padding-top: 1px;
             padding-bottom: 3px;
             line-height: $font-22px;
         }
@@ -105,16 +105,9 @@ $left-gutter: 64px;
         }
 
         .mx_EventTile_readAvatars {
-            top: 27px;
-        }
-
-        &.mx_EventTile_continuation .mx_EventTile_readAvatars,
-        &.mx_EventTile_emote .mx_EventTile_readAvatars {
-            top: 5px;
-        }
-
-        &.mx_EventTile_info .mx_EventTile_readAvatars {
-            top: 4px;
+            // This aligns the avatar with the last line of the
+            // message. We want to move it one line up - 2rem
+            top: -2rem;
         }
 
         .mx_EventTile_content .markdown-body {
diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss
index ece547d02b..21baa795e6 100644
--- a/res/css/views/rooms/_IRCLayout.scss
+++ b/res/css/views/rooms/_IRCLayout.scss
@@ -181,8 +181,7 @@ $irc-line-height: $font-18px;
         > span {
             display: flex;
 
-            > .mx_SenderProfile_name,
-            > .mx_SenderProfile_aux {
+            > .mx_SenderProfile_name {
                 overflow: hidden;
                 text-overflow: ellipsis;
                 min-width: var(--name-width);
@@ -207,6 +206,16 @@ $irc-line-height: $font-18px;
             width: unset;
             max-width: var(--name-width);
         }
+
+        .mx_SenderProfile_hover {
+            background: transparent;
+
+            > span {
+                > .mx_SenderProfile_name {
+                    min-width: inherit;
+                }
+            }
+        }
     }
 
     .mx_ProfileResizer {
diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss
index 182c280217..3f7f83d334 100644
--- a/res/css/views/rooms/_MemberInfo.scss
+++ b/res/css/views/rooms/_MemberInfo.scss
@@ -19,6 +19,7 @@ limitations under the License.
     flex-direction: column;
     flex: 1;
     overflow-y: auto;
+    margin-top: 8px;
 }
 
 .mx_MemberInfo_name {
diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss
index 1e3506e371..075e9ff585 100644
--- a/res/css/views/rooms/_MemberList.scss
+++ b/res/css/views/rooms/_MemberList.scss
@@ -44,6 +44,12 @@ limitations under the License.
     .mx_AutoHideScrollbar {
         flex: 1 1 0;
     }
+
+    .mx_RightPanel_scopeHeader {
+        // vertically align with position on other right panel cards
+        // to prevent it bouncing as user navigates right panel
+        margin-top: -8px;
+    }
 }
 
 .mx_GroupMemberList_query,
diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index 71c0db947e..e6c0cc3f46 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -227,16 +227,8 @@ limitations under the License.
     mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
 }
 
-.mx_MessageComposer_hangup::before {
-    mask-image: url('$(res)/img/element-icons/call/hangup.svg');
-}
-
-.mx_MessageComposer_voicecall::before {
-    mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
-}
-
-.mx_MessageComposer_videocall::before {
-    mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+.mx_MessageComposer_voiceMessage::before {
+    mask-image: url('$(res)/img/voip/mic-on-mask.svg');
 }
 
 .mx_MessageComposer_emoji::before {
@@ -247,6 +239,32 @@ limitations under the License.
     mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
 }
 
+.mx_MessageComposer_sendMessage {
+    cursor: pointer;
+    position: relative;
+    margin-right: 6px;
+    width: 32px;
+    height: 32px;
+    border-radius: 100%;
+    background-color: $button-bg-color;
+
+    &::before {
+        position: absolute;
+        height: 16px;
+        width: 16px;
+        top: 8px;
+        left: 9px;
+
+        mask-image: url('$(res)/img/element-icons/send-message.svg');
+        mask-repeat: no-repeat;
+        mask-size: contain;
+        mask-position: center;
+
+        background-color: $button-fg-color;
+        content: '';
+    }
+}
+
 .mx_MessageComposer_formatting {
     cursor: pointer;
     margin: 0 11px;
diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss
index d97c49630a..b305e91db0 100644
--- a/res/css/views/rooms/_MessageComposerFormatBar.scss
+++ b/res/css/views/rooms/_MessageComposerFormatBar.scss
@@ -60,6 +60,8 @@ limitations under the License.
         width: 27px;
         height: 24px;
         box-sizing: border-box;
+        background: none;
+        vertical-align: middle;
     }
 
     .mx_MessageComposerFormatBar_button::after {
diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss
index 4322ba341c..9c2a428cb3 100644
--- a/res/css/views/rooms/_NewRoomIntro.scss
+++ b/res/css/views/rooms/_NewRoomIntro.scss
@@ -33,8 +33,13 @@ limitations under the License.
 
         .mx_AccessibleButton {
             line-height: $font-24px;
+            display: inline-block;
 
-            &::before {
+            & + .mx_AccessibleButton {
+                margin-left: 12px;
+            }
+
+            &:not(.mx_AccessibleButton_kind_primary_outline)::before {
                 content: '';
                 display: inline-block;
                 background-color: $button-fg-color;
diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss
index a23a44906f..387d1588a3 100644
--- a/res/css/views/rooms/_RoomHeader.scss
+++ b/res/css/views/rooms/_RoomHeader.scss
@@ -252,6 +252,19 @@ limitations under the License.
     mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
 }
 
+.mx_RoomHeader_voiceCallButton::before {
+    mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
+
+    // The call button SVG is padded slightly differently, so match it up to the size
+    // of the other icons
+    mask-size: 20px;
+    mask-position: center;
+}
+
+.mx_RoomHeader_videoCallButton::before {
+    mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+}
+
 .mx_RoomHeader_showPanel {
     height: 16px;
 }
diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss
index b7759d265f..8eda25d0c9 100644
--- a/res/css/views/rooms/_RoomList.scss
+++ b/res/css/views/rooms/_RoomList.scss
@@ -19,38 +19,51 @@ limitations under the License.
 }
 
 .mx_RoomList_iconPlus::before {
-    mask-image: url('$(res)/img/element-icons/roomlist/plus.svg');
+    mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg');
+}
+.mx_RoomList_iconHash::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg');
 }
 .mx_RoomList_iconExplore::before {
     mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
 }
+.mx_RoomList_iconBrowse::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
+}
+.mx_RoomList_iconDialpad::before {
+    mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg');
+}
 
 .mx_RoomList_explorePrompt {
     margin: 4px 12px 4px;
     padding-top: 12px;
-    border-top: 1px solid $tertiary-fg-color;
-    font-size: $font-13px;
+    border-top: 1px solid $input-border-color;
+    font-size: $font-14px;
 
     div:first-child {
         font-weight: $font-semi-bold;
+        line-height: $font-18px;
+        color: $primary-fg-color;
     }
 
     .mx_AccessibleButton {
-        color: $secondary-fg-color;
+        color: $primary-fg-color;
         position: relative;
-        padding: 0 0 0 24px;
+        padding: 8px 8px 8px 32px;
         font-size: inherit;
-        margin-top: 8px;
+        margin-top: 12px;
         display: block;
         text-align: start;
+        background-color: $roomlist-button-bg-color;
+        border-radius: 4px;
 
         &::before {
             content: '';
             width: 16px;
             height: 16px;
             position: absolute;
-            top: 0;
-            left: 0;
+            top: 8px;
+            left: 8px;
             background: $secondary-fg-color;
             mask-position: center;
             mask-size: contain;
@@ -64,5 +77,13 @@ limitations under the License.
         &.mx_RoomList_explorePrompt_explore::before {
             mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
         }
+
+        &.mx_RoomList_explorePrompt_spaceInvite::before {
+            mask-image: url('$(res)/img/element-icons/room/invite.svg');
+        }
+
+        &.mx_RoomList_explorePrompt_spaceExplore::before {
+            mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
+        }
     }
 }
diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss
index 0b1da7a41c..b340080837 100644
--- a/res/css/views/rooms/_RoomPreviewBar.scss
+++ b/res/css/views/rooms/_RoomPreviewBar.scss
@@ -40,6 +40,35 @@ limitations under the License.
         word-break: break-word;
     }
 
+    .mx_RoomPreviewBar_reason {
+        text-align: left;
+        background-color: $primary-bg-color;
+        border: 1px solid $invite-reason-border-color;
+        border-radius: 10px;
+        padding: 0 16px 12px 16px;
+        margin: 5px 0 20px 0;
+
+        div {
+            pointer-events: none;
+        }
+
+        .mx_EventTile_msgOption {
+            display: none;
+        }
+
+        .mx_MatrixChat_useCompactLayout & {
+            padding-top: 9px;
+        }
+
+        &.mx_EventTilePreview_faded {
+            cursor: pointer;
+
+            .mx_SenderProfile, .mx_EventTile_avatar {
+                opacity: 0.3;
+            }
+        }
+    }
+
     .mx_Spinner {
         width: auto;
         height: auto;
diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss
index 27c7c7d0f7..92a475694e 100644
--- a/res/css/views/rooms/_RoomSublist.scss
+++ b/res/css/views/rooms/_RoomSublist.scss
@@ -197,6 +197,9 @@ limitations under the License.
 
         .mx_RoomSublist_resizerHandles {
             flex: 0 0 4px;
+            display: flex;
+            justify-content: center;
+            width: 100%;
         }
 
         // Class name comes from the ResizableBox component
@@ -207,17 +210,12 @@ limitations under the License.
             border-radius: 3px;
 
             // Override styles from library
-            width: unset !important;
+            max-width: 64px;
             height: 4px !important; // Update RESIZE_HANDLE_HEIGHT if this changes
 
             // This is positioned directly below the 'show more' button.
-            position: absolute;
+            position: relative !important;
             bottom: 0 !important; // override from library
-
-            // Together, these make the bar 64px wide
-            // These are also overridden from the library
-            left: calc(50% - 32px) !important;
-            right: calc(50% - 32px) !important;
         }
 
         &:hover, &.mx_RoomSublist_hasMenuOpen {
diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss
index 8eca3f1efa..72d29dfd4c 100644
--- a/res/css/views/rooms/_RoomTile.scss
+++ b/res/css/views/rooms/_RoomTile.scss
@@ -189,6 +189,10 @@ limitations under the License.
         mask-image: url('$(res)/img/element-icons/settings.svg');
     }
 
+    .mx_RoomTile_iconInvite::before {
+        mask-image: url('$(res)/img/element-icons/room/invite.svg');
+    }
+
     .mx_RoomTile_iconSignOut::before {
         mask-image: url('$(res)/img/element-icons/leave.svg');
     }
diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss
new file mode 100644
index 0000000000..2fb112a38c
--- /dev/null
+++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss
@@ -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.
+*/
+
+.mx_VoiceRecordComposerTile_stop {
+    // 28px plus a 2px border makes this a 32px square (as intended)
+    width: 28px;
+    height: 28px;
+    border: 2px solid $voice-record-stop-border-color;
+    border-radius: 32px;
+    margin-right: 16px; // between us and the send button
+    position: relative;
+
+    &::after {
+        content: '';
+        width: 14px;
+        height: 14px;
+        position: absolute;
+        top: 7px;
+        left: 7px;
+        border-radius: 2px;
+        background-color: $voice-record-stop-symbol-color;
+    }
+}
+
+.mx_VoiceRecordComposerTile_waveformContainer {
+    padding: 5px;
+    padding-right: 4px; // there's 1px from the waveform itself, so account for that
+    padding-left: 15px; // +10px for the live circle, +5px for regular padding
+    background-color: $voice-record-waveform-bg-color;
+    border-radius: 12px;
+    margin-right: 12px; // isolate from stop button
+
+    // Cheat at alignment a bit
+    display: flex;
+    align-items: center;
+
+    position: relative; // important for the live circle
+
+    color: $voice-record-waveform-fg-color;
+    font-size: $font-14px;
+
+    &::before {
+        // TODO: @@ TravisR: Animate
+        content: '';
+        background-color: $voice-record-live-circle-color;
+        width: 10px;
+        height: 10px;
+        position: absolute;
+        left: 8px;
+        top: 16px; // vertically center
+        border-radius: 10px;
+    }
+
+    .mx_Waveform_bar {
+        background-color: $voice-record-waveform-fg-color;
+    }
+
+    .mx_Clock {
+        padding-right: 8px; // isolate from waveform
+        padding-left: 10px; // isolate from live circle
+        width: 42px; // we're not using a monospace font, so fake it
+    }
+}
diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss
index e6d09b9a2a..77a7bc5b68 100644
--- a/res/css/views/settings/_Notifications.scss
+++ b/res/css/views/settings/_Notifications.scss
@@ -64,6 +64,7 @@ limitations under the License.
 
 .mx_UserNotifSettings_notifTable {
     display: table;
+    position: relative;
 }
 
 .mx_UserNotifSettings_notifTable .mx_Spinner {
diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss
index 732cbedf02..4cbcb8e708 100644
--- a/res/css/views/settings/_ProfileSettings.scss
+++ b/res/css/views/settings/_ProfileSettings.scss
@@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+.mx_ProfileSettings_controls_topic {
+    & > textarea {
+        resize: vertical;
+    }
+}
+
 .mx_ProfileSettings_profile {
     display: flex;
 }
diff --git a/res/css/views/settings/_SpellCheckLanguages.scss b/res/css/views/settings/_SpellCheckLanguages.scss
new file mode 100644
index 0000000000..bb322c983f
--- /dev/null
+++ b/res/css/views/settings/_SpellCheckLanguages.scss
@@ -0,0 +1,35 @@
+/*
+Copyright 2021 Šimon Brandner 
+
+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_ExistingSpellCheckLanguage {
+    display: flex;
+    align-items: center;
+    margin-bottom: 5px;
+}
+
+.mx_ExistingSpellCheckLanguage_language {
+    flex: 1;
+    margin-right: 10px;
+}
+
+.mx_GeneralUserSettingsTab_spellCheckLanguageInput {
+    margin-top: 1em;
+    margin-bottom: 1em;
+}
+
+.mx_SpellCheckLanguages {
+    @mixin mx_Settings_fullWidthField;
+}
diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss
new file mode 100644
index 0000000000..204ccab2b7
--- /dev/null
+++ b/res/css/views/spaces/_SpaceBasicSettings.scss
@@ -0,0 +1,86 @@
+/*
+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_SpaceBasicSettings {
+    .mx_Field {
+        margin: 32px 0;
+    }
+
+    .mx_SpaceBasicSettings_avatarContainer {
+        display: flex;
+        margin-top: 24px;
+
+        .mx_SpaceBasicSettings_avatar {
+            position: relative;
+            height: 80px;
+            width: 80px;
+            background-color: $tertiary-fg-color;
+            border-radius: 16px;
+        }
+
+        img.mx_SpaceBasicSettings_avatar {
+            width: 80px;
+            height: 80px;
+            object-fit: cover;
+            border-radius: 16px;
+        }
+
+        // only show it when the button is a div and not an img (has avatar)
+        div.mx_SpaceBasicSettings_avatar {
+            cursor: pointer;
+
+            &::before {
+                content: "";
+                position: absolute;
+                height: 80px;
+                width: 80px;
+                top: 0;
+                left: 0;
+                background-color: #ffffff; // white icon fill
+                mask-repeat: no-repeat;
+                mask-position: center;
+                mask-size: 20px;
+                mask-image: url('$(res)/img/element-icons/camera.svg');
+            }
+        }
+
+        > input[type="file"] {
+            display: none;
+        }
+
+        > .mx_AccessibleButton_kind_link {
+            display: inline-block;
+            padding: 0;
+            margin: auto 16px;
+            color: #368bd6;
+        }
+
+        > .mx_SpaceBasicSettings_avatar_remove {
+            color: $notice-primary-color;
+        }
+    }
+
+    .mx_FormButton {
+        padding: 8px 22px;
+        margin-left: auto;
+        display: block;
+        width: min-content;
+    }
+
+    .mx_AccessibleButton_disabled {
+        cursor: not-allowed;
+    }
+}
diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss
new file mode 100644
index 0000000000..ef3fea351b
--- /dev/null
+++ b/res/css/views/spaces/_SpaceCreateMenu.scss
@@ -0,0 +1,93 @@
+/*
+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.
+*/
+
+$spacePanelWidth: 71px;
+
+.mx_SpaceCreateMenu_wrapper {
+    // background blur everything except SpacePanel
+    .mx_ContextualMenu_background {
+        background-color: $dialog-backdrop-color;
+        opacity: 0.6;
+        left: $spacePanelWidth;
+    }
+
+    .mx_ContextualMenu {
+        padding: 24px;
+        width: 480px;
+        box-sizing: border-box;
+        background-color: $primary-bg-color;
+
+        > div {
+            > h2 {
+                font-weight: $font-semi-bold;
+                font-size: $font-18px;
+                margin-top: 4px;
+            }
+
+            > p {
+                font-size: $font-15px;
+                color: $secondary-fg-color;
+                margin: 0;
+            }
+        }
+
+        .mx_SpaceCreateMenuType {
+            @mixin SpacePillButton;
+        }
+
+        .mx_SpaceCreateMenuType_public::before {
+            mask-image: url('$(res)/img/globe.svg');
+        }
+        .mx_SpaceCreateMenuType_private::before {
+            mask-image: url('$(res)/img/element-icons/lock.svg');
+        }
+
+        .mx_SpaceCreateMenu_back {
+            width: 28px;
+            height: 28px;
+            position: relative;
+            background-color: $theme-button-bg-color;
+            border-radius: 14px;
+            margin-bottom: 12px;
+
+            &::before {
+                content: "";
+                position: absolute;
+                height: 28px;
+                width: 28px;
+                top: 0;
+                left: 0;
+                background-color: $muted-fg-color;
+                transform: rotate(90deg);
+                mask-repeat: no-repeat;
+                mask-position: 2px 3px;
+                mask-size: 24px;
+                mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
+            }
+        }
+
+        .mx_AccessibleButton_kind_primary {
+            padding: 8px 22px;
+            margin-left: auto;
+            display: block;
+            width: min-content;
+        }
+
+        .mx_AccessibleButton_disabled {
+            cursor: not-allowed;
+        }
+    }
+}
diff --git a/res/css/views/spaces/_SpacePublicShare.scss b/res/css/views/spaces/_SpacePublicShare.scss
new file mode 100644
index 0000000000..373fa94e00
--- /dev/null
+++ b/res/css/views/spaces/_SpacePublicShare.scss
@@ -0,0 +1,29 @@
+/*
+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_SpacePublicShare {
+    .mx_AccessibleButton {
+        @mixin SpacePillButton;
+
+        &.mx_SpacePublicShare_shareButton::before {
+            mask-image: url('$(res)/img/element-icons/link.svg');
+        }
+
+        &.mx_SpacePublicShare_inviteButton::before {
+            mask-image: url('$(res)/img/element-icons/room/invite.svg');
+        }
+    }
+}
diff --git a/res/css/views/voice_messages/_Waveform.scss b/res/css/views/voice_messages/_Waveform.scss
new file mode 100644
index 0000000000..cf03c84601
--- /dev/null
+++ b/res/css/views/voice_messages/_Waveform.scss
@@ -0,0 +1,40 @@
+/*
+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_Waveform {
+    position: relative;
+    height: 30px; // tallest bar can only be 30px
+    top: 1px; // because of our border trick (see below), we're off by 1px of aligntment
+
+    display: flex;
+    align-items: center; // so the bars grow from the middle
+
+    overflow: hidden; // this is cheaper than a `max-height: calc(100% - 4px)` in the bar's CSS.
+
+    // A bar is meant to be a 2x2 circle when at zero height, and otherwise a 2px wide line
+    // with rounded caps.
+    .mx_Waveform_bar {
+        width: 0; // 0px width means we'll end up using the border as our width
+        border: 1px solid transparent; // transparent means we'll use the background colour
+        border-radius: 2px; // rounded end caps, based on the border
+        min-height: 0; // like the width, we'll rely on the border to give us height
+        max-height: 100%; // this makes the `height: 42%` work on the element
+        margin-left: 1px; // we want 2px between each bar, so 1px on either side for balance
+        margin-right: 1px;
+
+        // background color is handled by the parent components
+    }
+}
diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss
index eec8a1f188..8262075559 100644
--- a/res/css/views/voip/_CallContainer.scss
+++ b/res/css/views/voip/_CallContainer.scss
@@ -18,10 +18,7 @@ limitations under the License.
     position: absolute;
     right: 20px;
     bottom: 72px;
-    border-radius: 8px;
-    overflow: hidden;
     z-index: 100;
-    box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
 
     // Disable pointer events for Jitsi widgets to function. Direct
     // calls have their own cursor and behaviour, but we need to make
@@ -49,8 +46,10 @@ limitations under the License.
 
     .mx_IncomingCallBox {
         min-width: 250px;
-        background-color: $primary-bg-color;
+        background-color: $voipcall-plinth-color;
         padding: 8px;
+        box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
+        border-radius: 8px;
 
         pointer-events: initial; // restore pointer events so the user can accept/decline
         cursor: pointer;
diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index dbe2c27e41..e032e4582d 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -35,6 +35,10 @@ limitations under the License.
 
 .mx_CallView_pip {
     width: 320px;
+    padding-bottom: 8px;
+    margin-top: 10px;
+    box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
+    border-radius: 8px;
 
     .mx_CallView_voice {
         height: 180px;
@@ -51,7 +55,7 @@ limitations under the License.
         }
     }
 
-    .mx_CallView_voice_holdText {
+    .mx_CallView_holdTransferContent {
         padding-top: 10px;
         padding-bottom: 25px;
     }
@@ -78,60 +82,16 @@ limitations under the License.
     }
 }
 
-.mx_CallView_voice_hold {
+.mx_CallView_voice .mx_CallView_holdTransferContent {
     // This masks the avatar image so when it's blurred, the edge is still crisp
     .mx_CallView_voice_avatarContainer {
         border-radius: 2000px;
         overflow: hidden;
         position: relative;
-        &::after {
-            position: absolute;
-            content: '';
-            width: 100%;
-            height: 100%;
-            top: 50%;
-            left: 50%;
-            transform: translate(-50%, -50%);
-            background-color: rgba(0, 0, 0, 0.6);
-            background-image: url('$(res)/img/voip/paused.svg');
-            background-position: center;
-            background-size: 40px;
-            background-repeat: no-repeat;
-        }
-        .mx_CallView_pip &::after {
-            background-size: 30px;
-        }
-    }
-    .mx_BaseAvatar {
-        filter: blur(20px);
-        overflow: hidden;
     }
 }
 
-.mx_CallView_voice_secondaryAvatarContainer {
-    border-radius: 2000px;
-    overflow: hidden;
-    position: relative;
-    &::after {
-        position: absolute;
-        content: '';
-        width: 100%;
-        height: 100%;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
-        background-color: rgba(0, 0, 0, 0.6);
-        background-image: url('$(res)/img/voip/paused.svg');
-        background-position: center;
-        background-size: 40px;
-        background-repeat: no-repeat;
-    }
-    .mx_CallView_pip &::after {
-        background-size: 24px;
-    }
-}
-
-.mx_CallView_voice_holdText {
+.mx_CallView_holdTransferContent {
     height: 20px;
     padding-top: 20px;
     padding-bottom: 15px;
@@ -182,7 +142,7 @@ limitations under the License.
     }
 }
 
-.mx_CallView_video_holdContent {
+.mx_CallView_video .mx_CallView_holdTransferContent {
     position: absolute;
     top: 50%;
     left: 50%;
@@ -350,8 +310,14 @@ limitations under the License.
     }
 }
 
-// Makes the alignment correct
-.mx_CallView_callControls_nothing {
+.mx_CallView_callControls_dialpad {
+    margin-right: auto;
+    &::before {
+        background-image: url('$(res)/img/voip/dialpad.svg');
+    }
+}
+
+.mx_CallView_callControls_button_dialpad_hidden {
     margin-right: auto;
     cursor: initial;
 }
diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss
new file mode 100644
index 0000000000..0c7bff0ce8
--- /dev/null
+++ b/res/css/views/voip/_DialPad.scss
@@ -0,0 +1,62 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_DialPad {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 16px;
+}
+
+.mx_DialPad_button {
+    width: 40px;
+    height: 40px;
+    background-color: $theme-button-bg-color;
+    border-radius: 40px;
+    font-size: 18px;
+    font-weight: 600;
+    text-align: center;
+    vertical-align: middle;
+    line-height: 40px;
+}
+
+.mx_DialPad_deleteButton, .mx_DialPad_dialButton {
+    &::before {
+        content: '';
+        display: inline-block;
+        height: 40px;
+        width: 40px;
+        vertical-align: middle;
+        mask-repeat: no-repeat;
+        mask-size: 20px;
+        mask-position: center;
+        background-color: $primary-bg-color;
+    }
+}
+
+.mx_DialPad_deleteButton {
+    background-color: $notice-primary-color;
+    &::before {
+        mask-image: url('$(res)/img/element-icons/call/delete.svg');
+        mask-position: 9px; // delete icon is right-heavy so have to be slightly to the left to look centered
+    }
+}
+
+.mx_DialPad_dialButton {
+    background-color: $accent-color;
+    &::before {
+        mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
+    }
+}
diff --git a/src/utils/NamingUtils.ts b/res/css/views/voip/_DialPadContextMenu.scss
similarity index 50%
rename from src/utils/NamingUtils.ts
rename to res/css/views/voip/_DialPadContextMenu.scss
index 44ccb9b030..520f51cf93 100644
--- a/src/utils/NamingUtils.ts
+++ b/res/css/views/voip/_DialPadContextMenu.scss
@@ -14,16 +14,34 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import * as projectNameGenerator from "project-name-generator";
-
-/**
- * Generates a human readable identifier. This should not be used for anything
- * which needs secure/cryptographic random: just a level uniquness that is offered
- * by something like Date.now().
- * @returns {string} The randomly generated ID
- */
-export function generateHumanReadableId(): string {
-    return projectNameGenerator({words: 3}).raw.map(w => {
-        return w[0].toUpperCase() + w.substring(1).toLowerCase();
-    }).join('');
+.mx_DialPadContextMenu_header {
+    margin-top: 12px;
+    margin-left: 12px;
+    margin-right: 12px;
+}
+
+.mx_DialPadContextMenu_title {
+    color: $muted-fg-color;
+    font-size: 12px;
+    font-weight: 600;
+}
+
+.mx_DialPadContextMenu_dialled {
+    height: 1em;
+    font-size: 18px;
+    font-weight: 600;
+}
+
+.mx_DialPadContextMenu_dialPad {
+    margin: 16px;
+}
+
+.mx_DialPadContextMenu_horizSep {
+    position: relative;
+    &::before {
+        content: '';
+        position: absolute;
+        width: 100%;
+        border-bottom: 1px solid $input-darker-bg-color;
+    }
 }
diff --git a/res/css/views/voip/_DialPadModal.scss b/res/css/views/voip/_DialPadModal.scss
new file mode 100644
index 0000000000..f9d7673a38
--- /dev/null
+++ b/res/css/views/voip/_DialPadModal.scss
@@ -0,0 +1,74 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_Dialog_dialPadWrapper .mx_Dialog {
+    padding: 0px;
+}
+
+.mx_DialPadModal {
+    width: 192px;
+    height: 368px;
+}
+
+.mx_DialPadModal_header {
+    margin-top: 12px;
+    margin-left: 12px;
+    margin-right: 12px;
+}
+
+.mx_DialPadModal_title {
+    color: $muted-fg-color;
+    font-size: 12px;
+    font-weight: 600;
+}
+
+.mx_DialPadModal_cancel {
+    float: right;
+    mask: url('$(res)/img/feather-customised/cancel.svg');
+    mask-repeat: no-repeat;
+    mask-position: center;
+    mask-size: cover;
+    width: 14px;
+    height: 14px;
+    background-color: $dialog-close-fg-color;
+    cursor: pointer;
+}
+
+.mx_DialPadModal_field {
+    border: none;
+    margin: 0px;
+}
+
+.mx_DialPadModal_field input {
+    font-size: 18px;
+    font-weight: 600;
+}
+
+.mx_DialPadModal_dialPad {
+    margin-left: 16px;
+    margin-right: 16px;
+    margin-top: 16px;
+}
+
+.mx_DialPadModal_horizSep {
+    position: relative;
+    &::before {
+        content: '';
+        position: absolute;
+        width: 100%;
+        border-bottom: 1px solid $input-darker-bg-color;
+    }
+}
diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss
index 931410dba3..3e473a80b2 100644
--- a/res/css/views/voip/_VideoFeed.scss
+++ b/res/css/views/voip/_VideoFeed.scss
@@ -16,6 +16,7 @@ limitations under the License.
 
 .mx_VideoFeed_remote {
     width: 100%;
+    max-height: 100%;
     background-color: #000;
     z-index: 50;
 }
diff --git a/res/img/element-icons/brands/apple.svg b/res/img/element-icons/brands/apple.svg
new file mode 100644
index 0000000000..308c3c5d5a
--- /dev/null
+++ b/res/img/element-icons/brands/apple.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/brands/element.svg b/res/img/element-icons/brands/element.svg
new file mode 100644
index 0000000000..6861de0955
--- /dev/null
+++ b/res/img/element-icons/brands/element.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/res/img/element-icons/brands/facebook.svg b/res/img/element-icons/brands/facebook.svg
new file mode 100644
index 0000000000..2742785424
--- /dev/null
+++ b/res/img/element-icons/brands/facebook.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/res/img/element-icons/brands/github.svg b/res/img/element-icons/brands/github.svg
new file mode 100644
index 0000000000..503719520b
--- /dev/null
+++ b/res/img/element-icons/brands/github.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/brands/gitlab.svg b/res/img/element-icons/brands/gitlab.svg
new file mode 100644
index 0000000000..df84c41e21
--- /dev/null
+++ b/res/img/element-icons/brands/gitlab.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/res/img/element-icons/brands/google.svg b/res/img/element-icons/brands/google.svg
new file mode 100644
index 0000000000..1b0b19ae5b
--- /dev/null
+++ b/res/img/element-icons/brands/google.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/res/img/element-icons/brands/twitter.svg b/res/img/element-icons/brands/twitter.svg
new file mode 100644
index 0000000000..43eb825a59
--- /dev/null
+++ b/res/img/element-icons/brands/twitter.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/call/delete.svg b/res/img/element-icons/call/delete.svg
new file mode 100644
index 0000000000..133bdad4ca
--- /dev/null
+++ b/res/img/element-icons/call/delete.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/res/img/element-icons/circle-sending.svg b/res/img/element-icons/circle-sending.svg
new file mode 100644
index 0000000000..2d15a0f716
--- /dev/null
+++ b/res/img/element-icons/circle-sending.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/circle-sent.svg b/res/img/element-icons/circle-sent.svg
new file mode 100644
index 0000000000..04a00ceff7
--- /dev/null
+++ b/res/img/element-icons/circle-sent.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/res/img/element-icons/expand-space-panel.svg b/res/img/element-icons/expand-space-panel.svg
new file mode 100644
index 0000000000..11232acd58
--- /dev/null
+++ b/res/img/element-icons/expand-space-panel.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/res/img/element-icons/link.svg b/res/img/element-icons/link.svg
new file mode 100644
index 0000000000..ab3d54b838
--- /dev/null
+++ b/res/img/element-icons/link.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/lock.svg b/res/img/element-icons/lock.svg
new file mode 100644
index 0000000000..06fe52a391
--- /dev/null
+++ b/res/img/element-icons/lock.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/plus.svg b/res/img/element-icons/plus.svg
new file mode 100644
index 0000000000..ea1972237d
--- /dev/null
+++ b/res/img/element-icons/plus.svg
@@ -0,0 +1,3 @@
+
+    
+
diff --git a/res/img/element-icons/room/invite.svg b/res/img/element-icons/room/invite.svg
index 655f9f118a..d2ecb837b2 100644
--- a/res/img/element-icons/room/invite.svg
+++ b/res/img/element-icons/room/invite.svg
@@ -1,3 +1,3 @@
-
+
 
 
diff --git a/res/img/element-icons/roomlist/browse.svg b/res/img/element-icons/roomlist/browse.svg
new file mode 100644
index 0000000000..04714e2881
--- /dev/null
+++ b/res/img/element-icons/roomlist/browse.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/res/img/element-icons/roomlist/dialpad.svg b/res/img/element-icons/roomlist/dialpad.svg
new file mode 100644
index 0000000000..b51d4a4dc9
--- /dev/null
+++ b/res/img/element-icons/roomlist/dialpad.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/roomlist/hash-circle.svg b/res/img/element-icons/roomlist/hash-circle.svg
new file mode 100644
index 0000000000..924b22cf32
--- /dev/null
+++ b/res/img/element-icons/roomlist/hash-circle.svg
@@ -0,0 +1,7 @@
+
+    
+        
+    
+    
+    
+
diff --git a/res/img/element-icons/roomlist/plus-circle.svg b/res/img/element-icons/roomlist/plus-circle.svg
new file mode 100644
index 0000000000..251ded225c
--- /dev/null
+++ b/res/img/element-icons/roomlist/plus-circle.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/send-message.svg b/res/img/element-icons/send-message.svg
new file mode 100644
index 0000000000..ce35bf8bc8
--- /dev/null
+++ b/res/img/element-icons/send-message.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/element-icons/upload.svg b/res/img/element-icons/upload.svg
new file mode 100644
index 0000000000..71ad7ba1cf
--- /dev/null
+++ b/res/img/element-icons/upload.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/res/img/element-icons/warning-badge.svg b/res/img/element-icons/warning-badge.svg
index ac5991f221..1ae4e40ffe 100644
--- a/res/img/element-icons/warning-badge.svg
+++ b/res/img/element-icons/warning-badge.svg
@@ -1,5 +1,5 @@
 
-
-
-
+    
+    
+    
 
diff --git a/res/img/feather-customised/widget/maximise.svg b/res/img/feather-customised/maximise.svg
similarity index 100%
rename from res/img/feather-customised/widget/maximise.svg
rename to res/img/feather-customised/maximise.svg
diff --git a/res/img/feather-customised/widget/minimise.svg b/res/img/feather-customised/minimise.svg
similarity index 100%
rename from res/img/feather-customised/widget/minimise.svg
rename to res/img/feather-customised/minimise.svg
diff --git a/res/img/feather-customised/monitor.svg b/res/img/feather-customised/monitor.svg
deleted file mode 100644
index 231811d5a6..0000000000
--- a/res/img/feather-customised/monitor.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/res/img/feather-customised/smartphone.svg b/res/img/feather-customised/smartphone.svg
deleted file mode 100644
index fde78c82e2..0000000000
--- a/res/img/feather-customised/smartphone.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/res/img/upload-big.svg b/res/img/upload-big.svg
index 6099c2e976..9a6a265fdb 100644
--- a/res/img/upload-big.svg
+++ b/res/img/upload-big.svg
@@ -1,19 +1,3 @@
-
-
-    
-    icons_upload_drop
-    Created with bin/sketchtool.
-    
-    
-        
-            
-                
-                    
-                    
-                    
-                
-                
-            
-        
-    
+
+
 
diff --git a/res/img/voip/dialpad.svg b/res/img/voip/dialpad.svg
new file mode 100644
index 0000000000..79c9ba1612
--- /dev/null
+++ b/res/img/voip/dialpad.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/img/voip/mic-on-mask.svg b/res/img/voip/mic-on-mask.svg
new file mode 100644
index 0000000000..418316b164
--- /dev/null
+++ b/res/img/voip/mic-on-mask.svg
@@ -0,0 +1,3 @@
+
+    
+
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 08fe2e9f57..f7fda92346 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -137,9 +137,6 @@ $panel-divider-color: transparent;
 $widget-menu-bar-bg-color: $header-panel-bg-color;
 $widget-body-bg-color: rgba(141, 151, 165, 0.2);
 
-// event tile lifecycle
-$event-sending-color: $text-secondary-color;
-
 // event redaction
 $event-redacted-fg-color: #606060;
 $event-redacted-border-color: #000000;
@@ -172,6 +169,9 @@ $button-link-bg-color: transparent;
 // Toggle switch
 $togglesw-off-color: $room-highlight-color;
 
+$progressbar-fg-color: $accent-color;
+$progressbar-bg-color: #21262c;
+
 $visual-bell-bg-color: #800;
 
 $room-warning-bg-color: $header-panel-bg-color;
@@ -202,9 +202,15 @@ $breadcrumb-placeholder-bg-color: #272c35;
 
 $user-tile-hover-bg-color: $header-panel-bg-color;
 
+$message-body-panel-bg-color: #21262c82;
+$message-body-panel-icon-bg-color: #8e99a4;
+$message-body-panel-fg-color: $primary-fg-color;
+
 // Appearance tab colors
 $appearance-tab-border-color: $room-highlight-color;
 
+$invite-reason-border-color: $room-highlight-color;
+
 // blur amounts for left left panel (only for element theme, used in _mods.scss)
 $roomlist-background-blur-amount: 60px;
 $groupFilterPanel-background-blur-amount: 30px;
@@ -258,6 +264,12 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
 // markdown overrides:
 .mx_EventTile_content .markdown-body pre:hover {
     border-color: #808080 !important; // inverted due to rules below
+    scrollbar-color: rgba(0, 0, 0, 0.2) transparent; // copied from light theme due to inversion below
+    // the code above works only in Firefox, this is for other browsers
+    // see https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
+    &::-webkit-scrollbar-thumb {
+        background-color: rgba(0, 0, 0, 0.2); // copied from light theme due to inversion below
+    }
 }
 .mx_EventTile_content .markdown-body {
     pre, code {
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 3e3c299af9..95c558a0e4 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -132,9 +132,6 @@ $panel-divider-color: $header-panel-border-color;
 $widget-menu-bar-bg-color: $header-panel-bg-color;
 $widget-body-bg-color: #1A1D23;
 
-// event tile lifecycle
-$event-sending-color: $text-secondary-color;
-
 // event redaction
 $event-redacted-fg-color: #606060;
 $event-redacted-border-color: #000000;
@@ -167,6 +164,9 @@ $button-link-bg-color: transparent;
 // Toggle switch
 $togglesw-off-color: $room-highlight-color;
 
+$progressbar-fg-color: $accent-color;
+$progressbar-bg-color: #21262c;
+
 $visual-bell-bg-color: #800;
 
 $room-warning-bg-color: $header-panel-bg-color;
@@ -197,9 +197,15 @@ $breadcrumb-placeholder-bg-color: #272c35;
 
 $user-tile-hover-bg-color: $header-panel-bg-color;
 
+$message-body-panel-bg-color: #21262c82;
+$message-body-panel-icon-bg-color: #8e99a4;
+$message-body-panel-fg-color: $primary-fg-color;
+
 // Appearance tab colors
 $appearance-tab-border-color: $room-highlight-color;
 
+$invite-reason-border-color: $room-highlight-color;
+
 $composer-shadow-color: tranparent;
 
 // ***** Mixins! *****
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 085d6d7f10..a3f83cabe0 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -188,6 +188,12 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
 
 $groupFilterPanel-divider-color: $roomlist-header-color;
 
+$voice-record-stop-border-color: #E3E8F0;
+$voice-record-stop-symbol-color: $warning-color;
+$voice-record-waveform-bg-color: #E3E8F0;
+$voice-record-waveform-fg-color: $muted-fg-color;
+$voice-record-live-circle-color: $warning-color;
+
 $roomtile-preview-color: #9e9e9e;
 $roomtile-default-badge-bg-color: #61708b;
 $roomtile-selected-bg-color: #fff;
@@ -222,8 +228,6 @@ $widget-body-bg-color: #fff;
 $yellow-background: #fff8e3;
 
 // event tile lifecycle
-$event-encrypting-color: #abddbc;
-$event-sending-color: #ddd;
 $event-notsent-color: #f44;
 
 $event-highlight-fg-color: $warning-color;
@@ -237,7 +241,8 @@ $event-redacted-border-color: #cccccc;
 $event-timestamp-color: #acacac;
 
 $copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
-
+$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
+$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
 
 // e2e
 $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
@@ -280,7 +285,8 @@ $togglesw-ball-color: #fff;
 $slider-selection-color: $accent-color;
 $slider-background-color: #c1c9d6;
 
-$progressbar-color: #000;
+$progressbar-fg-color: $accent-color;
+$progressbar-bg-color: rgba(141, 151, 165, 0.2);
 
 $room-warning-bg-color: $yellow-background;
 
@@ -320,9 +326,15 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
 
 $user-tile-hover-bg-color: $header-panel-bg-color;
 
+$message-body-panel-bg-color: #e3e8f082;
+$message-body-panel-icon-bg-color: #ffffff;
+$message-body-panel-fg-color: $muted-fg-color;
+
 // FontSlider colors
 $appearance-tab-border-color: $input-darker-bg-color;
 
+$invite-reason-border-color: $input-darker-bg-color;
+
 $composer-shadow-color: tranparent;
 
 // ***** Mixins! *****
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 4cfeeae05e..3465f555b6 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -67,9 +67,6 @@ $groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77);
 // used by RoomDirectory permissions
 $plinth-bg-color: $secondary-accent-color;
 
-// used by RoomDropTarget
-$droptarget-bg-color: rgba(255,255,255,0.5);
-
 // used by AddressSelector
 $selected-color: $secondary-accent-color;
 
@@ -182,6 +179,12 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
 
 $groupFilterPanel-divider-color: $roomlist-header-color;
 
+$voice-record-stop-border-color: #E3E8F0;
+$voice-record-stop-symbol-color: $warning-color;
+$voice-record-waveform-bg-color: #E3E8F0;
+$voice-record-waveform-fg-color: $muted-fg-color;
+$voice-record-live-circle-color: $warning-color;
+
 $roomtile-preview-color: $secondary-fg-color;
 $roomtile-default-badge-bg-color: #61708b;
 $roomtile-selected-bg-color: #FFF;
@@ -222,8 +225,6 @@ $widget-body-bg-color: #FFF;
 $yellow-background: #fff8e3;
 
 // event tile lifecycle
-$event-encrypting-color: #abddbc;
-$event-sending-color: #ddd;
 $event-notsent-color: #f44;
 
 $event-highlight-fg-color: $warning-color;
@@ -237,6 +238,8 @@ $event-redacted-border-color: #cccccc;
 $event-timestamp-color: #acacac;
 
 $copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
+$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
+$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
 
 // e2e
 $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
@@ -279,7 +282,8 @@ $togglesw-ball-color: #fff;
 $slider-selection-color: $accent-color;
 $slider-background-color: #c1c9d6;
 
-$progressbar-color: #000;
+$progressbar-fg-color: $accent-color;
+$progressbar-bg-color: rgba(141, 151, 165, 0.2);
 
 $room-warning-bg-color: $yellow-background;
 
@@ -320,9 +324,15 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
 
 $user-tile-hover-bg-color: $header-panel-bg-color;
 
+$message-body-panel-bg-color: #e3e8f082;
+$message-body-panel-icon-bg-color: #ffffff;
+$message-body-panel-fg-color: $muted-fg-color;
+
 // FontSlider colors
 $appearance-tab-border-color: $input-darker-bg-color;
 
+$invite-reason-border-color: $input-darker-bg-color;
+
 // blur amounts for left left panel (only for element theme, used in _mods.scss)
 $roomlist-background-blur-amount: 40px;
 $groupFilterPanel-background-blur-amount: 20px;
diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss
index 30aaeedf8f..fbca58dfb1 100644
--- a/res/themes/light/css/_mods.scss
+++ b/res/themes/light/css/_mods.scss
@@ -16,6 +16,10 @@
         backdrop-filter: blur($groupFilterPanel-background-blur-amount);
     }
 
+    .mx_SpacePanel {
+        backdrop-filter: blur($groupFilterPanel-background-blur-amount);
+    }
+
     .mx_LeftPanel .mx_LeftPanel_roomListContainer {
         backdrop-filter: blur($roomlist-background-blur-amount);
     }
diff --git a/scripts/ci/Dockerfile b/scripts/ci/Dockerfile
index 5351291f29..3fdd0d7bf6 100644
--- a/scripts/ci/Dockerfile
+++ b/scripts/ci/Dockerfile
@@ -1,8 +1,7 @@
 # Update on docker hub with the following commands in the directory of this file:
 # docker build -t vectorim/element-web-ci-e2etests-env:latest .
-# docker log
 # docker push vectorim/element-web-ci-e2etests-env:latest
-FROM node:10
+FROM node:14-buster
 RUN apt-get update
 RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
 # dependencies for chrome (installed by puppeteer)
diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh
index 14b5fc5393..bbda74ef9d 100755
--- a/scripts/ci/install-deps.sh
+++ b/scripts/ci/install-deps.sh
@@ -7,7 +7,6 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk
 pushd matrix-js-sdk
 yarn link
 yarn install $@
-yarn build
 popd
 
 yarn link matrix-js-sdk
diff --git a/scripts/ci/layered.sh b/scripts/ci/layered.sh
index 306f9c9974..039f90c7df 100755
--- a/scripts/ci/layered.sh
+++ b/scripts/ci/layered.sh
@@ -20,6 +20,7 @@ popd
 yarn link matrix-js-sdk
 yarn link
 yarn install
+yarn reskindex
 
 # Finally, set up element-web
 scripts/fetchdep.sh vector-im element-web
diff --git a/scripts/reskindex.js b/scripts/reskindex.js
index 9fb0e1a7c0..5eaec4d1d5 100755
--- a/scripts/reskindex.js
+++ b/scripts/reskindex.js
@@ -1,29 +1,33 @@
 #!/usr/bin/env node
-var fs = require('fs');
-var path = require('path');
-var glob = require('glob');
-var args = require('minimist')(process.argv);
-var chokidar = require('chokidar');
+const fs = require('fs');
+const { promises: fsp } = fs;
+const path = require('path');
+const glob = require('glob');
+const util = require('util');
+const args = require('minimist')(process.argv);
+const chokidar = require('chokidar');
 
-var componentIndex = path.join('src', 'component-index.js');
-var componentIndexTmp = componentIndex+".tmp";
-var componentsDir = path.join('src', 'components');
-var componentJsGlob = '**/*.js';
-var componentTsGlob = '**/*.tsx';
-var prevFiles = [];
+const componentIndex = path.join('src', 'component-index.js');
+const componentIndexTmp = componentIndex+".tmp";
+const componentsDir = path.join('src', 'components');
+const componentJsGlob = '**/*.js';
+const componentTsGlob = '**/*.tsx';
+let prevFiles = [];
 
-function reskindex() {
-    var jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort();
-    var tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort();
-    var files = [...tsFiles, ...jsFiles];
+async function reskindex() {
+    const jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort();
+    const tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort();
+    const files = [...tsFiles, ...jsFiles];
     if (!filesHaveChanged(files, prevFiles)) {
         return;
     }
     prevFiles = files;
 
-    var header = args.h || args.header;
+    const header = args.h || args.header;
 
-    var strm = fs.createWriteStream(componentIndexTmp);
+    const strm = fs.createWriteStream(componentIndexTmp);
+    // Wait for the open event to ensure the file descriptor is set
+    await new Promise(resolve => strm.once("open", resolve));
 
     if (header) {
        strm.write(fs.readFileSync(header));
@@ -38,11 +42,11 @@ function reskindex() {
     strm.write(" */\n\n");
     strm.write("let components = {};\n");
 
-    for (var i = 0; i < files.length; ++i) {
-        var file = files[i].replace('.js', '').replace('.tsx', '');
+    for (let i = 0; i < files.length; ++i) {
+        const file = files[i].replace('.js', '').replace('.tsx', '');
 
-        var moduleName = (file.replace(/\//g, '.'));
-        var importName = moduleName.replace(/\./g, "$");
+        const moduleName = (file.replace(/\//g, '.'));
+        const importName = moduleName.replace(/\./g, "$");
 
         strm.write("import " + importName + " from './components/" + file + "';\n");
         strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");");
@@ -51,14 +55,10 @@ function reskindex() {
     }
 
     strm.write("export {components};\n");
-    strm.end();
-    fs.rename(componentIndexTmp, componentIndex, function(err) {
-        if(err) {
-            console.error("Error moving new index into place: " + err);
-        } else {
-            console.log('Reskindex: completed');
-        }
-    });
+    // Ensure the file has been fully written to disk before proceeding
+    await util.promisify(fs.fsync)(strm.fd);
+    await util.promisify(strm.end);
+    await fsp.rename(componentIndexTmp, componentIndex);
 }
 
 // Expects both arrays of file names to be sorted
@@ -67,7 +67,7 @@ function filesHaveChanged(files, prevFiles) {
         return true;
     }
     // Check for name changes
-    for (var i = 0; i < files.length; i++) {
+    for (let i = 0; i < files.length; i++) {
         if (prevFiles[i] !== files[i]) {
             return true;
         }
@@ -75,15 +75,23 @@ function filesHaveChanged(files, prevFiles) {
     return false;
 }
 
+// Wrapper since await at the top level is not well supported yet
+function run() {
+    (async function() {
+        await reskindex();
+        console.log("Reskindex completed");
+    })();
+}
+
 // -w indicates watch mode where any FS events will trigger reskindex
 if (!args.w) {
-    reskindex();
+    run();
     return;
 }
 
-var watchDebouncer = null;
+let watchDebouncer = null;
 chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
     if (path === componentIndex) return;
     if (watchDebouncer) clearTimeout(watchDebouncer);
-    watchDebouncer = setTimeout(reskindex, 1000);
+    watchDebouncer = setTimeout(run, 1000);
 });
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 741798761f..051e5cc429 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -36,6 +36,10 @@ import {Analytics} from "../Analytics";
 import CountlyAnalytics from "../CountlyAnalytics";
 import UserActivity from "../UserActivity";
 import {ModalWidgetStore} from "../stores/ModalWidgetStore";
+import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
+import VoipUserMapper from "../VoipUserMapper";
+import {SpaceStoreClass} from "../stores/SpaceStore";
+import {VoiceRecorder} from "../voice/VoiceRecorder";
 
 declare global {
     interface Window {
@@ -59,11 +63,15 @@ declare global {
         mxNotifier: typeof Notifier;
         mxRightPanelStore: RightPanelStore;
         mxWidgetStore: WidgetStore;
+        mxWidgetLayoutStore: WidgetLayoutStore;
         mxCallHandler: CallHandler;
         mxAnalytics: Analytics;
         mxCountlyAnalytics: typeof CountlyAnalytics;
         mxUserActivity: UserActivity;
         mxModalWidgetStore: ModalWidgetStore;
+        mxVoipUserMapper: VoipUserMapper;
+        mxSpaceStore: SpaceStoreClass;
+        mxVoiceRecorder: typeof VoiceRecorder;
     }
 
     interface Document {
diff --git a/src/Avatar.ts b/src/Avatar.ts
index 60bdfdcf75..76c88faa1c 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -14,27 +14,23 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
 import {RoomMember} from "matrix-js-sdk/src/models/room-member";
 import {User} from "matrix-js-sdk/src/models/user";
 import {Room} from "matrix-js-sdk/src/models/room";
 
-import {MatrixClientPeg} from './MatrixClientPeg';
 import DMRoomMap from './utils/DMRoomMap';
+import {mediaFromMxc} from "./customisations/Media";
 
 export type ResizeMethod = "crop" | "scale";
 
 // Not to be used for BaseAvatar urls as that has similar default avatar fallback already
 export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
     let url: string;
-    if (member && member.getAvatarUrl) {
-        url = member.getAvatarUrl(
-            MatrixClientPeg.get().getHomeserverUrl(),
+    if (member?.getMxcAvatarUrl()) {
+        url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
             Math.floor(width * window.devicePixelRatio),
             Math.floor(height * window.devicePixelRatio),
             resizeMethod,
-            false,
-            false,
         );
     }
     if (!url) {
@@ -47,16 +43,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu
 }
 
 export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
-    const url = getHttpUriForMxc(
-        MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
+    if (!user.avatarUrl) return null;
+    return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(
         Math.floor(width * window.devicePixelRatio),
         Math.floor(height * window.devicePixelRatio),
         resizeMethod,
     );
-    if (!url || url.length === 0) {
-        return null;
-    }
-    return url;
 }
 
 function isValidHexColor(color: string): boolean {
@@ -154,17 +146,13 @@ export function getInitialLetter(name: string): string {
 export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
     if (!room) return null; // null-guard
 
-    const explicitRoomAvatar = room.getAvatarUrl(
-        MatrixClientPeg.get().getHomeserverUrl(),
-        width,
-        height,
-        resizeMethod,
-        false,
-    );
-    if (explicitRoomAvatar) {
-        return explicitRoomAvatar;
+    if (room.getMxcAvatarUrl()) {
+        return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
     }
 
+    // space rooms cannot be DMs so skip the rest
+    if (room.isSpaceRoom()) return null;
+
     let otherMember = null;
     const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
     if (otherUserId) {
@@ -174,14 +162,8 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
         // then still try to show any avatar (pref. other member)
         otherMember = room.getAvatarFallbackMember();
     }
-    if (otherMember) {
-        return otherMember.getAvatarUrl(
-            MatrixClientPeg.get().getHomeserverUrl(),
-            width,
-            height,
-            resizeMethod,
-            false,
-        );
+    if (otherMember?.getMxcAvatarUrl()) {
+        return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
     }
     return null;
 }
diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index c301aa6a10..b6012d7597 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -30,6 +30,7 @@ import {idbLoad, idbSave, idbDelete} from "./utils/StorageManager";
 
 export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
 export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
+export const SSO_IDP_ID_KEY = "mx_sso_idp_id";
 
 export enum UpdateCheckStatus {
     Checking = "CHECKING",
@@ -56,7 +57,7 @@ export default abstract class BasePlatform {
         this.startUpdateCheck = this.startUpdateCheck.bind(this);
     }
 
-    abstract async getConfig(): Promise<{}>;
+    abstract getConfig(): Promise<{}>;
 
     abstract getDefaultDeviceDisplayName(): string;
 
@@ -130,6 +131,14 @@ export default abstract class BasePlatform {
         hideUpdateToast();
     }
 
+    /**
+     * Return true if platform supports multi-language
+     * spell-checking, otherwise false.
+     */
+    supportsMultiLanguageSpellCheck(): boolean {
+        return false;
+    }
+
     /**
      * Returns true if the platform supports displaying
      * notifications, otherwise false.
@@ -203,6 +212,18 @@ export default abstract class BasePlatform {
         throw new Error("Unimplemented");
     }
 
+    supportsWarnBeforeExit(): boolean {
+        return false;
+    }
+
+    async shouldWarnBeforeExit(): Promise {
+        return false;
+    }
+
+    async setWarnBeforeExit(enabled: boolean): Promise {
+        throw new Error("Unimplemented");
+    }
+
     supportsAutoHideMenuBar(): boolean {
         return false;
     }
@@ -239,6 +260,16 @@ export default abstract class BasePlatform {
 
     setLanguage(preferredLangs: string[]) {}
 
+    setSpellCheckLanguages(preferredLangs: string[]) {}
+
+    getSpellCheckLanguages(): Promise | null {
+        return null;
+    }
+
+    getAvailableSpellCheckLanguages(): Promise | null {
+        return null;
+    }
+
     protected getSSOCallbackUrl(fragmentAfterLogin: string): URL {
         const url = new URL(window.location.href);
         url.hash = fragmentAfterLogin || "";
@@ -258,6 +289,9 @@ export default abstract class BasePlatform {
         if (mxClient.getIdentityServerUrl()) {
             localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
         }
+        if (idpId) {
+            localStorage.setItem(SSO_IDP_ID_KEY, idpId);
+        }
         const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
         window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO
     }
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 7160657c07..be687a4474 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -64,7 +64,6 @@ import dis from './dispatcher/dispatcher';
 import WidgetUtils from './utils/WidgetUtils';
 import WidgetEchoStore from './stores/WidgetEchoStore';
 import SettingsStore from './settings/SettingsStore';
-import {generateHumanReadableId} from "./utils/NamingUtils";
 import {Jitsi} from "./widgets/Jitsi";
 import {WidgetType} from "./widgets/WidgetType";
 import {SettingLevel} from "./settings/SettingLevel";
@@ -82,6 +81,21 @@ import CountlyAnalytics from "./CountlyAnalytics";
 import {UIFeature} from "./settings/UIFeature";
 import { CallError } from "matrix-js-sdk/src/webrtc/call";
 import { logger } from 'matrix-js-sdk/src/logger';
+import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"
+import { Action } from './dispatcher/actions';
+import VoipUserMapper from './VoipUserMapper';
+import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
+import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring";
+
+export const PROTOCOL_PSTN = 'm.protocol.pstn';
+export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
+export const PROTOCOL_SIP_NATIVE = 'im.vector.protocol.sip_native';
+export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual';
+
+const CHECK_PROTOCOLS_ATTEMPTS = 3;
+// Event type for room account data and room creation content used to mark rooms as virtual rooms
+// (and store the ID of their native room)
+export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
 
 enum AudioID {
     Ring = 'ringAudio',
@@ -90,6 +104,29 @@ enum AudioID {
     Busy = 'busyAudio',
 }
 
+interface ThirdpartyLookupResponseFields {
+    /* eslint-disable camelcase */
+
+    // im.vector.sip_native
+    virtual_mxid?: string;
+    is_virtual?: boolean;
+
+    // im.vector.sip_virtual
+    native_mxid?: string;
+    is_native?: boolean;
+
+    // common
+    lookup_success?: boolean;
+
+    /* eslint-enable camelcase */
+}
+
+interface ThirdpartyLookupResponse {
+    userid: string,
+    protocol: string,
+    fields: ThirdpartyLookupResponseFields,
+}
+
 // Unlike 'CallType' in js-sdk, this one includes screen sharing
 // (because a screen sharing call is only a screen sharing call to the caller,
 // to the callee it's just a video call, at least as far as the current impl
@@ -117,8 +154,18 @@ function getRemoteAudioElement(): HTMLAudioElement {
 
 export default class CallHandler {
     private calls = new Map(); // roomId -> call
+    // Calls started as an attended transfer, ie. with the intention of transferring another
+    // call with a different party to this one.
+    private transferees = new Map(); // callId (target) -> call (transferee)
     private audioPromises = new Map>();
     private dispatcherRef: string = null;
+    private supportsPstnProtocol = null;
+    private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
+    private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
+    private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser
+    // For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't.
+    private invitedRoomsAreVirtual = new Map();
+    private invitedRoomCheckInProgress = false;
 
     static sharedInstance() {
         if (!window.mxCallHandler) {
@@ -128,6 +175,15 @@ export default class CallHandler {
         return window.mxCallHandler;
     }
 
+    /*
+     * Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
+     * if a voip_mxid_translate_pattern is set in the config)
+     */
+    public static roomIdForCall(call: MatrixCall): string {
+        if (!call) return null;
+        return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId;
+    }
+
     start() {
         this.dispatcherRef = dis.register(this.onAction);
         // add empty handlers for media actions, otherwise the media keys
@@ -145,6 +201,8 @@ export default class CallHandler {
         if (SettingsStore.getValue(UIFeature.Voip)) {
             MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming);
         }
+
+        this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
     }
 
     stop() {
@@ -158,6 +216,73 @@ export default class CallHandler {
         }
     }
 
+    private async checkProtocols(maxTries) {
+        try {
+            const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
+
+            if (protocols[PROTOCOL_PSTN] !== undefined) {
+                this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN]);
+                if (this.supportsPstnProtocol) this.pstnSupportPrefixed = false;
+            } else if (protocols[PROTOCOL_PSTN_PREFIXED] !== undefined) {
+                this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN_PREFIXED]);
+                if (this.supportsPstnProtocol) this.pstnSupportPrefixed = true;
+            } else {
+                this.supportsPstnProtocol = null;
+            }
+
+            dis.dispatch({action: Action.PstnSupportUpdated});
+
+            if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) {
+                this.supportsSipNativeVirtual = Boolean(
+                    protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL],
+                );
+            }
+
+            dis.dispatch({action: Action.VirtualRoomSupportUpdated});
+        } catch (e) {
+            if (maxTries === 1) {
+                console.log("Failed to check for protocol support and no retries remain: assuming no support", e);
+            } else {
+                console.log("Failed to check for protocol support: will retry", e);
+                this.pstnSupportCheckTimer = setTimeout(() => {
+                    this.checkProtocols(maxTries - 1);
+                }, 10000);
+            }
+        }
+    }
+
+    public getSupportsPstnProtocol() {
+        return this.supportsPstnProtocol;
+    }
+
+    public getSupportsVirtualRooms() {
+        return this.supportsPstnProtocol;
+    }
+
+    public pstnLookup(phoneNumber: string): Promise {
+        return MatrixClientPeg.get().getThirdpartyUser(
+            this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN, {
+                'm.id.phone': phoneNumber,
+            },
+        );
+    }
+
+    public sipVirtualLookup(nativeMxid: string): Promise {
+        return MatrixClientPeg.get().getThirdpartyUser(
+            PROTOCOL_SIP_VIRTUAL, {
+                'native_mxid': nativeMxid,
+            },
+        );
+    }
+
+    public sipNativeLookup(virtualMxid: string): Promise {
+        return MatrixClientPeg.get().getThirdpartyUser(
+            PROTOCOL_SIP_NATIVE, {
+                'virtual_mxid': virtualMxid,
+            },
+        );
+    }
+
     private onCallIncoming = (call) => {
         // we dispatch this synchronously to make sure that the event
         // handlers on the call are set up immediately (so that if
@@ -203,6 +328,10 @@ export default class CallHandler {
         return callsNotInThatRoom;
     }
 
+    getTransfereeForCallId(callId: string): MatrixCall {
+        return this.transferees[callId];
+    }
+
     play(audioId: AudioID) {
         // TODO: Attach an invisible element for this instead
         // which listens?
@@ -250,11 +379,15 @@ export default class CallHandler {
         // We don't allow placing more than one call per room, but that doesn't mean there
         // can't be more than one, eg. in a glare situation. This checks that the given call
         // is the call we consider 'the' call for its room.
-        const callForThisRoom = this.getCallForRoom(call.roomId);
+        const mappedRoomId = CallHandler.roomIdForCall(call);
+
+        const callForThisRoom = this.getCallForRoom(mappedRoomId);
         return callForThisRoom && call.callId === callForThisRoom.callId;
     }
 
     private setCallListeners(call: MatrixCall) {
+        const mappedRoomId = CallHandler.roomIdForCall(call);
+
         call.on(CallEvent.Error, (err: CallError) => {
             if (!this.matchesCallForThisRoom(call)) return;
 
@@ -284,7 +417,7 @@ export default class CallHandler {
 
             Analytics.trackEvent('voip', 'callHangup');
 
-            this.removeCallForRoom(call.roomId);
+            this.removeCallForRoom(mappedRoomId);
         });
         call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
             if (!this.matchesCallForThisRoom(call)) return;
@@ -308,8 +441,9 @@ export default class CallHandler {
                     this.play(AudioID.Ringback);
                     break;
                 case CallState.Ended:
+                {
                     Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
-                    this.removeCallForRoom(call.roomId);
+                    this.removeCallForRoom(mappedRoomId);
                     if (oldState === CallState.InviteSent && (
                         call.hangupParty === CallParty.Remote ||
                         (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
@@ -341,10 +475,14 @@ export default class CallHandler {
                             title: _t("Answered Elsewhere"),
                             description: _t("The call was answered on another device."),
                         });
-                    } else if (oldState !== CallState.Fledgling) {
+                    } else if (oldState !== CallState.Fledgling && oldState !== CallState.Ringing) {
                         // don't play the end-call sound for calls that never got off the ground
                         this.play(AudioID.CallEnd);
                     }
+
+                    this.logCallStats(call, mappedRoomId);
+                    break;
+                }
             }
         });
         call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
@@ -358,25 +496,70 @@ export default class CallHandler {
                 this.pause(AudioID.Ringback);
             }
 
-            this.calls.set(newCall.roomId, newCall);
+            this.calls.set(mappedRoomId, newCall);
             this.setCallListeners(newCall);
             this.setCallState(newCall, newCall.state);
         });
     }
 
+    private async logCallStats(call: MatrixCall, mappedRoomId: string) {
+        const stats = await call.getCurrentCallStats();
+        logger.debug(
+            `Call completed. Call ID: ${call.callId}, virtual room ID: ${call.roomId}, ` +
+            `user-facing room ID: ${mappedRoomId}, direction: ${call.direction}, ` +
+            `our Party ID: ${call.ourPartyId}, hangup party: ${call.hangupParty}, ` +
+            `hangup reason: ${call.hangupReason}`,
+        );
+        if (!stats) {
+            logger.debug(
+                "Call statistics are undefined. The call has " +
+                "probably failed before a peerConn was established",
+            );
+            return;
+        }
+        logger.debug("Local candidates:");
+        for (const cand of stats.filter(item => item.type === 'local-candidate')) {
+            const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
+            logger.debug(
+                `${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
+                `protocol: ${cand.protocol}, relay protocol: ${cand.relayProtocol}, network type: ${cand.networkType}`,
+            );
+        }
+        logger.debug("Remote candidates:");
+        for (const cand of stats.filter(item => item.type === 'remote-candidate')) {
+            const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
+            logger.debug(
+                `${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
+                `protocol: ${cand.protocol}`,
+            );
+        }
+        logger.debug("Candidate pairs:");
+        for (const pair of stats.filter(item => item.type === 'candidate-pair')) {
+            logger.debug(
+                `${pair.localCandidateId} / ${pair.remoteCandidateId} - state: ${pair.state}, ` +
+                `nominated: ${pair.nominated}, ` +
+                `requests sent ${pair.requestsSent}, requests received  ${pair.requestsReceived},  ` +
+                `responses received: ${pair.responsesReceived}, responses sent: ${pair.responsesSent}, ` +
+                `bytes received: ${pair.bytesReceived}, bytes sent: ${pair.bytesSent}, `,
+            );
+        }
+    }
+
     private setCallAudioElement(call: MatrixCall) {
         const audioElement = getRemoteAudioElement();
         if (audioElement) call.setRemoteAudioElement(audioElement);
     }
 
     private setCallState(call: MatrixCall, status: CallState) {
+        const mappedRoomId = CallHandler.roomIdForCall(call);
+
         console.log(
-            `Call state in ${call.roomId} changed to ${status}`,
+            `Call state in ${mappedRoomId} changed to ${status}`,
         );
 
         dis.dispatch({
             action: 'call_state',
-            room_id: call.roomId,
+            room_id: mappedRoomId,
             state: status,
         });
     }
@@ -443,14 +626,26 @@ export default class CallHandler {
         }, null, true);
     }
 
-    private placeCall(
+    private async placeCall(
         roomId: string, type: PlaceCallType,
         localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
+        transferee: MatrixCall,
     ) {
         Analytics.trackEvent('voip', 'placeCall', 'type', type);
         CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
-        const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
+
+        const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId;
+        logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
+
+        const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
+        console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
+        const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
+
         this.calls.set(roomId, call);
+        if (transferee) {
+            this.transferees[call.callId] = transferee;
+        }
+
         this.setCallListeners(call);
         this.setCallAudioElement(call);
 
@@ -474,9 +669,17 @@ export default class CallHandler {
                 });
                 return;
             }
-            call.placeScreenSharingCall(remoteElement, localElement);
+
+            call.placeScreenSharingCall(
+                remoteElement,
+                localElement,
+                async () : Promise => {
+                    const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
+                    const [source] = await finished;
+                    return source;
+                });
         } else {
-            console.error("Unknown conf call type: %s", type);
+            console.error("Unknown conf call type: " + type);
         }
     }
 
@@ -484,6 +687,12 @@ export default class CallHandler {
         switch (payload.action) {
             case 'place_call':
                 {
+                    // We might be using managed hybrid widgets
+                    if (isManagedHybridWidgetEnabled()) {
+                        addManagedHybridWidget(payload.room_id);
+                        return;
+                    }
+
                     // if the runtime env doesn't do VoIP, whine.
                     if (!MatrixClientPeg.get().supportsVoip()) {
                         Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
@@ -504,7 +713,15 @@ export default class CallHandler {
 
                     const room = MatrixClientPeg.get().getRoom(payload.room_id);
                     if (!room) {
-                        console.error("Room %s does not exist.", payload.room_id);
+                        console.error(`Room ${payload.room_id} does not exist.`);
+                        return;
+                    }
+
+                    if (this.getCallForRoom(room.roomId)) {
+                        Modal.createTrackedDialog('Call Handler', 'Existing Call with user', ErrorDialog, {
+                            title: _t('Already in call'),
+                            description: _t("You're already in a call with this person."),
+                        });
                         return;
                     }
 
@@ -515,9 +732,12 @@ export default class CallHandler {
                         });
                         return;
                     } else if (members.length === 2) {
-                        console.info("Place %s call in %s", payload.type, payload.room_id);
+                        console.info(`Place ${payload.type} call in ${payload.room_id}`);
 
-                        this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
+                        this.placeCall(
+                            payload.room_id, payload.type, payload.local_element, payload.remote_element,
+                            payload.transferee,
+                        );
                     } else { // > 2
                         dis.dispatch({
                             action: "place_conference_call",
@@ -530,17 +750,17 @@ export default class CallHandler {
                 }
                 break;
             case 'place_conference_call':
-                console.info("Place conference call in %s", payload.room_id);
+                console.info("Place conference call in " + payload.room_id);
                 Analytics.trackEvent('voip', 'placeConferenceCall');
                 CountlyAnalytics.instance.trackStartCall(payload.room_id, payload.type === PlaceCallType.Video, true);
                 this.startCallApp(payload.room_id, payload.type);
                 break;
             case 'end_conference':
-                console.info("Terminating conference call in %s", payload.room_id);
+                console.info("Terminating conference call in " + payload.room_id);
                 this.terminateCallApp(payload.room_id);
                 break;
             case 'hangup_conference':
-                console.info("Leaving conference call in %s", payload.room_id);
+                console.info("Leaving conference call in "+ payload.room_id);
                 this.hangupCallApp(payload.room_id);
                 break;
             case 'incoming_call':
@@ -552,14 +772,21 @@ export default class CallHandler {
 
                     const call = payload.call as MatrixCall;
 
-                    if (this.getCallForRoom(call.roomId)) {
+                    const mappedRoomId = CallHandler.roomIdForCall(call);
+                    if (this.getCallForRoom(mappedRoomId)) {
                         // ignore multiple incoming calls to the same room
                         return;
                     }
 
                     Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
-                    this.calls.set(call.roomId, call)
+                    this.calls.set(mappedRoomId, call)
                     this.setCallListeners(call);
+
+                    // get ready to send encrypted events in the room, so if the user does answer
+                    // the call, we'll be ready to send. NB. This is the protocol-level room ID not
+                    // the mapped one: that's where we'll send the events.
+                    const cli = MatrixClientPeg.get();
+                    cli.prepareToEncrypt(cli.getRoom(call.roomId));
                 }
                 break;
             case 'hangup':
@@ -575,6 +802,11 @@ export default class CallHandler {
                 // don't remove the call yet: let the hangup event handler do it (otherwise it will throw
                 // the hangup event away)
                 break;
+            case 'hangup_all':
+                for (const call of this.calls.values()) {
+                    call.hangup(CallErrorCode.UserHangup, false);
+                }
+                break;
             case 'answer': {
                 if (!this.calls.has(payload.room_id)) {
                     return; // no call to answer
@@ -617,6 +849,18 @@ export default class CallHandler {
         }
     }
 
+    /**
+     * @returns true if we are currently in any call where we haven't put the remote party on hold
+     */
+    hasAnyUnheldCall() {
+        for (const call of this.calls.values()) {
+            if (call.state === CallState.Ended) continue;
+            if (!call.isRemoteOnHold()) return true;
+        }
+
+        return false;
+    }
+
     private async startCallApp(roomId: string, type: string) {
         dis.dispatch({
             action: 'appsDrawer',
@@ -646,8 +890,9 @@ export default class CallHandler {
             // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
             confId = base32.stringify(Buffer.from(roomId), { pad: false });
         } else {
-            // Create a random human readable conference ID
-            confId = `JitsiConference${generateHumanReadableId()}`;
+            // Create a random conference ID
+            const random = randomUppercaseString(1) + randomLowercaseString(23);
+            confId = 'Jitsi' + random;
         }
 
         let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
@@ -663,6 +908,7 @@ export default class CallHandler {
             isAudioOnly: type === 'voice',
             domain: jitsiDomain,
             auth: jitsiAuth,
+            roomName: room.name,
         };
 
         const widgetId = (
diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js
index 8d56467c57..7c7940cab5 100644
--- a/src/CallMediaHandler.js
+++ b/src/CallMediaHandler.js
@@ -14,9 +14,9 @@
  limitations under the License.
 */
 
-import * as Matrix from 'matrix-js-sdk';
 import SettingsStore from "./settings/SettingsStore";
 import {SettingLevel} from "./settings/SettingLevel";
+import {setMatrixCallAudioInput, setMatrixCallAudioOutput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
 
 export default {
     hasAnyLabeledDevices: async function() {
@@ -54,24 +54,24 @@ export default {
         const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
         const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
 
-        Matrix.setMatrixCallAudioOutput(audioOutDeviceId);
-        Matrix.setMatrixCallAudioInput(audioDeviceId);
-        Matrix.setMatrixCallVideoInput(videoDeviceId);
+        setMatrixCallAudioOutput(audioOutDeviceId);
+        setMatrixCallAudioInput(audioDeviceId);
+        setMatrixCallVideoInput(videoDeviceId);
     },
 
     setAudioOutput: function(deviceId) {
         SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
-        Matrix.setMatrixCallAudioOutput(deviceId);
+        setMatrixCallAudioOutput(deviceId);
     },
 
     setAudioInput: function(deviceId) {
         SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
-        Matrix.setMatrixCallAudioInput(deviceId);
+        setMatrixCallAudioInput(deviceId);
     },
 
     setVideoInput: function(deviceId) {
         SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
-        Matrix.setMatrixCallVideoInput(deviceId);
+        setMatrixCallVideoInput(deviceId);
     },
 
     getAudioOutput: function() {
diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx
index 5409a606de..95b45cce4a 100644
--- a/src/ContentMessages.tsx
+++ b/src/ContentMessages.tsx
@@ -32,6 +32,14 @@ import Spinner from "./components/views/elements/Spinner";
 import "blueimp-canvas-to-blob";
 import { Action } from "./dispatcher/actions";
 import CountlyAnalytics from "./CountlyAnalytics";
+import {
+    UploadCanceledPayload,
+    UploadErrorPayload,
+    UploadFinishedPayload,
+    UploadProgressPayload,
+    UploadStartedPayload,
+} from "./dispatcher/payloads/UploadPayload";
+import {IUpload} from "./models/IUpload";
 
 const MAX_WIDTH = 800;
 const MAX_HEIGHT = 600;
@@ -44,15 +52,6 @@ export class UploadCanceledError extends Error {}
 
 type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
 
-interface IUpload {
-    fileName: string;
-    roomId: string;
-    total: number;
-    loaded: number;
-    promise: Promise;
-    canceled?: boolean;
-}
-
 interface IMediaConfig {
     "m.upload.size"?: number;
 }
@@ -478,7 +477,7 @@ export default class ContentMessages {
         if (upload) {
             upload.canceled = true;
             MatrixClientPeg.get().cancelUpload(upload.promise);
-            dis.dispatch({action: 'upload_canceled', upload});
+            dis.dispatch({action: Action.UploadCanceled, upload});
         }
     }
 
@@ -497,7 +496,7 @@ export default class ContentMessages {
             content.info.mimetype = file.type;
         }
 
-        const prom = new Promise((resolve) => {
+        const prom = new Promise((resolve) => {
             if (file.type.indexOf('image/') === 0) {
                 content.msgtype = 'm.image';
                 infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
@@ -539,7 +538,7 @@ export default class ContentMessages {
             promise: prom,
         };
         this.inprogress.push(upload);
-        dis.dispatch({action: 'upload_started'});
+        dis.dispatch({action: Action.UploadStarted, upload});
 
         // Focus the composer view
         dis.fire(Action.FocusComposer);
@@ -547,7 +546,7 @@ export default class ContentMessages {
         function onProgress(ev) {
             upload.total = ev.total;
             upload.loaded = ev.loaded;
-            dis.dispatch({action: 'upload_progress', upload: upload});
+            dis.dispatch({action: Action.UploadProgress, upload});
         }
 
         let error;
@@ -601,9 +600,9 @@ export default class ContentMessages {
                 if (error && error.http_status === 413) {
                     this.mediaConfig = null;
                 }
-                dis.dispatch({action: 'upload_failed', upload, error});
+                dis.dispatch({action: Action.UploadFailed, upload, error});
             } else {
-                dis.dispatch({action: 'upload_finished', upload});
+                dis.dispatch({action: Action.UploadFinished, upload});
                 dis.dispatch({action: 'message_sent'});
             }
         });
diff --git a/src/CountlyAnalytics.ts b/src/CountlyAnalytics.ts
index b4727bc88b..974c08df18 100644
--- a/src/CountlyAnalytics.ts
+++ b/src/CountlyAnalytics.ts
@@ -840,7 +840,7 @@ export default class CountlyAnalytics {
         let endTime = CountlyAnalytics.getTimestamp();
         const cli = MatrixClientPeg.get();
         if (!cli.getRoom(roomId)) {
-            await new Promise(resolve => {
+            await new Promise(resolve => {
                 const handler = (room) => {
                     if (room.roomId === roomId) {
                         cli.off("Room", handler);
@@ -880,7 +880,7 @@ export default class CountlyAnalytics {
         let endTime = CountlyAnalytics.getTimestamp();
 
         if (!room.findEventById(eventId)) {
-            await new Promise(resolve => {
+            await new Promise(resolve => {
                 const handler = (ev) => {
                     if (ev.getId() === eventId) {
                         room.off("Room.localEchoUpdated", handler);
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 2301ad250b..1dc342fac5 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -32,10 +32,10 @@ import { AllHtmlEntities } from 'html-entities';
 import SettingsStore from './settings/SettingsStore';
 import cheerio from 'cheerio';
 
-import {MatrixClientPeg} from './MatrixClientPeg';
 import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
 import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
 import ReplyThread from "./components/views/elements/ReplyThread";
+import {mediaFromMxc} from "./customisations/Media";
 
 linkifyMatrix(linkify);
 
@@ -163,7 +163,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
             attribs.target = '_blank'; // by default
 
             const transformed = tryTransformPermalinkToLocalHref(attribs.href);
-            if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN)) {
+            if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.ELEMENT_URL_PATTERN)) {
                 attribs.href = transformed;
                 delete attribs.target;
             }
@@ -181,11 +181,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
         if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
             return { tagName, attribs: {}};
         }
-        attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
-            attribs.src,
-            attribs.width || 800,
-            attribs.height || 600,
-        );
+        const width = Number(attribs.width) || 800;
+        const height = Number(attribs.height) || 600;
+        attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height);
         return { tagName, attribs };
     },
     'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
@@ -239,6 +237,7 @@ const sanitizeHtmlParams: IExtendedSanitizeOptions = {
         'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub',
         'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
         'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
+        'details', 'summary',
     ],
     allowedAttributes: {
         // custom ones first:
@@ -422,6 +421,8 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
             if (SettingsStore.getValue("feature_latex_maths")) {
                 const phtml = cheerio.load(safeBody,
                     { _useHtmlParser2: true, decodeEntities: false })
+                // @ts-ignore - The types for `replaceWith` wrongly expect
+                // Cheerio instance to be returned.
                 phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
                     return katex.renderToString(
                         AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
@@ -438,13 +439,14 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
         delete sanitizeParams.textFilter;
     }
 
+    const contentBody = isDisplayedWithHtml ? safeBody : strippedBody;
     if (opts.returnString) {
-        return isDisplayedWithHtml ? safeBody : strippedBody;
+        return contentBody;
     }
 
     let emojiBody = false;
     if (!opts.disableBigEmoji && bodyHasEmoji) {
-        let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : '';
+        let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : '';
 
         // Ignore spaces in body text. Emojis with spaces in between should
         // still be counted as purely emoji messages.
diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js
index fbdb6812ee..1687adf13b 100644
--- a/src/IdentityAuthClient.js
+++ b/src/IdentityAuthClient.js
@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { createClient, SERVICE_TYPES } from 'matrix-js-sdk';
+import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
+import { createClient } from 'matrix-js-sdk/src/matrix';
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import Modal from './Modal';
@@ -165,6 +166,7 @@ export default class IdentityAuthClient {
             });
             const [confirmed] = await finished;
             if (confirmed) {
+                // eslint-disable-next-line react-hooks/rules-of-hooks
                 useDefaultIdentityServer();
             } else {
                 throw new AbortedIdentityActionError(
diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts
new file mode 100644
index 0000000000..ac9ef1f8cc
--- /dev/null
+++ b/src/KeyBindingsDefaults.ts
@@ -0,0 +1,407 @@
+/*
+Copyright 2021 Clemens Zeidler
+
+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 { AutocompleteAction, IKeyBindingsProvider, KeyBinding, MessageComposerAction, NavigationAction, RoomAction,
+    RoomListAction } from "./KeyBindingsManager";
+import { isMac, Key } from "./Keyboard";
+import SettingsStore from "./settings/SettingsStore";
+
+const messageComposerBindings = (): KeyBinding[] => {
+    const bindings: KeyBinding[] = [
+        {
+            action: MessageComposerAction.SelectPrevSendHistory,
+            keyCombo: {
+                key: Key.ARROW_UP,
+                altKey: true,
+                ctrlKey: true,
+            },
+        },
+        {
+            action: MessageComposerAction.SelectNextSendHistory,
+            keyCombo: {
+                key: Key.ARROW_DOWN,
+                altKey: true,
+                ctrlKey: true,
+            },
+        },
+        {
+            action: MessageComposerAction.EditPrevMessage,
+            keyCombo: {
+                key: Key.ARROW_UP,
+            },
+        },
+        {
+            action: MessageComposerAction.EditNextMessage,
+            keyCombo: {
+                key: Key.ARROW_DOWN,
+            },
+        },
+        {
+            action: MessageComposerAction.CancelEditing,
+            keyCombo: {
+                key: Key.ESCAPE,
+            },
+        },
+        {
+            action: MessageComposerAction.FormatBold,
+            keyCombo: {
+                key: Key.B,
+                ctrlOrCmd: true,
+            },
+        },
+        {
+            action: MessageComposerAction.FormatItalics,
+            keyCombo: {
+                key: Key.I,
+                ctrlOrCmd: true,
+            },
+        },
+        {
+            action: MessageComposerAction.FormatQuote,
+            keyCombo: {
+                key: Key.GREATER_THAN,
+                ctrlOrCmd: true,
+                shiftKey: true,
+            },
+        },
+        {
+            action: MessageComposerAction.EditUndo,
+            keyCombo: {
+                key: Key.Z,
+                ctrlOrCmd: true,
+            },
+        },
+        {
+            action: MessageComposerAction.MoveCursorToStart,
+            keyCombo: {
+                key: Key.HOME,
+                ctrlOrCmd: true,
+            },
+        },
+        {
+            action: MessageComposerAction.MoveCursorToEnd,
+            keyCombo: {
+                key: Key.END,
+                ctrlOrCmd: true,
+            },
+        },
+    ];
+    if (isMac) {
+        bindings.push({
+            action: MessageComposerAction.EditRedo,
+            keyCombo: {
+                key: Key.Z,
+                ctrlOrCmd: true,
+                shiftKey: true,
+            },
+        });
+    } else {
+        bindings.push({
+            action: MessageComposerAction.EditRedo,
+            keyCombo: {
+                key: Key.Y,
+                ctrlOrCmd: true,
+            },
+        });
+    }
+    if (SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend')) {
+        bindings.push({
+            action: MessageComposerAction.Send,
+            keyCombo: {
+                key: Key.ENTER,
+                ctrlOrCmd: true,
+            },
+        });
+        bindings.push({
+            action: MessageComposerAction.NewLine,
+            keyCombo: {
+                key: Key.ENTER,
+            },
+        });
+    } else {
+        bindings.push({
+            action: MessageComposerAction.Send,
+            keyCombo: {
+                key: Key.ENTER,
+            },
+        });
+        bindings.push({
+            action: MessageComposerAction.NewLine,
+            keyCombo: {
+                key: Key.ENTER,
+                shiftKey: true,
+            },
+        });
+        if (isMac) {
+            bindings.push({
+                action: MessageComposerAction.NewLine,
+                keyCombo: {
+                    key: Key.ENTER,
+                    altKey: true,
+                },
+            });
+        }
+    }
+    return bindings;
+}
+
+const autocompleteBindings = (): KeyBinding[] => {
+    return [
+        {
+            action: AutocompleteAction.CompleteOrNextSelection,
+            keyCombo: {
+                key: Key.TAB,
+            },
+        },
+        {
+            action: AutocompleteAction.CompleteOrNextSelection,
+            keyCombo: {
+                key: Key.TAB,
+                ctrlKey: true,
+            },
+        },
+        {
+            action: AutocompleteAction.CompleteOrPrevSelection,
+            keyCombo: {
+                key: Key.TAB,
+                shiftKey: true,
+            },
+        },
+        {
+            action: AutocompleteAction.CompleteOrPrevSelection,
+            keyCombo: {
+                key: Key.TAB,
+                ctrlKey: true,
+                shiftKey: true,
+            },
+        },
+        {
+            action: AutocompleteAction.Cancel,
+            keyCombo: {
+                key: Key.ESCAPE,
+            },
+        },
+        {
+            action: AutocompleteAction.PrevSelection,
+            keyCombo: {
+                key: Key.ARROW_UP,
+            },
+        },
+        {
+            action: AutocompleteAction.NextSelection,
+            keyCombo: {
+                key: Key.ARROW_DOWN,
+            },
+        },
+    ];
+}
+
+const roomListBindings = (): KeyBinding[] => {
+    return [
+        {
+            action: RoomListAction.ClearSearch,
+            keyCombo: {
+                key: Key.ESCAPE,
+            },
+        },
+        {
+            action: RoomListAction.PrevRoom,
+            keyCombo: {
+                key: Key.ARROW_UP,
+            },
+        },
+        {
+            action: RoomListAction.NextRoom,
+            keyCombo: {
+                key: Key.ARROW_DOWN,
+            },
+        },
+        {
+            action: RoomListAction.SelectRoom,
+            keyCombo: {
+                key: Key.ENTER,
+            },
+        },
+        {
+            action: RoomListAction.CollapseSection,
+            keyCombo: {
+                key: Key.ARROW_LEFT,
+            },
+        },
+        {
+            action: RoomListAction.ExpandSection,
+            keyCombo: {
+                key: Key.ARROW_RIGHT,
+            },
+        },
+    ];
+}
+
+const roomBindings = (): KeyBinding[] => {
+    const bindings: KeyBinding[] = [
+        {
+            action: RoomAction.ScrollUp,
+            keyCombo: {
+                key: Key.PAGE_UP,
+            },
+        },
+        {
+            action: RoomAction.RoomScrollDown,
+            keyCombo: {
+                key: Key.PAGE_DOWN,
+            },
+        },
+        {
+            action: RoomAction.DismissReadMarker,
+            keyCombo: {
+                key: Key.ESCAPE,
+            },
+        },
+        {
+            action: RoomAction.JumpToOldestUnread,
+            keyCombo: {
+                key: Key.PAGE_UP,
+                shiftKey: true,
+            },
+        },
+        {
+            action: RoomAction.UploadFile,
+            keyCombo: {
+                key: Key.U,
+                ctrlOrCmd: true,
+                shiftKey: true,
+            },
+        },
+        {
+            action: RoomAction.JumpToFirstMessage,
+            keyCombo: {
+                key: Key.HOME,
+                ctrlKey: true,
+            },
+        },
+        {
+            action: RoomAction.JumpToLatestMessage,
+            keyCombo: {
+                key: Key.END,
+                ctrlKey: true,
+            },
+        },
+    ];
+
+    if (SettingsStore.getValue('ctrlFForSearch')) {
+        bindings.push({
+            action: RoomAction.FocusSearch,
+            keyCombo: {
+                key: Key.F,
+                ctrlOrCmd: true,
+            },
+        });
+    }
+
+    return bindings;
+}
+
+const navigationBindings = (): KeyBinding[] => {
+    return [
+        {
+            action: NavigationAction.FocusRoomSearch,
+            keyCombo: {
+                key: Key.K,
+                ctrlOrCmd: true,
+            },
+        },
+        {
+            action: NavigationAction.ToggleRoomSidePanel,
+            keyCombo: {
+                key: Key.PERIOD,
+                ctrlOrCmd: true,
+            },
+        },
+        {
+            action: NavigationAction.ToggleUserMenu,
+            // Ideally this would be CTRL+P for "Profile", but that's
+            // taken by the print dialog. CTRL+I for "Information"
+            // was previously chosen but conflicted with italics in
+            // composer, so CTRL+` it is
+            keyCombo: {
+                key: Key.BACKTICK,
+                ctrlOrCmd: true,
+            },
+        },
+        {
+            action: NavigationAction.ToggleShortCutDialog,
+            keyCombo: {
+                key: Key.SLASH,
+                ctrlOrCmd: true,
+            },
+        },
+        {
+            action: NavigationAction.ToggleShortCutDialog,
+            keyCombo: {
+                key: Key.SLASH,
+                ctrlOrCmd: true,
+                shiftKey: true,
+            },
+        },
+        {
+            action: NavigationAction.GoToHome,
+            keyCombo: {
+                key: Key.H,
+                ctrlOrCmd: true,
+                altKey: true,
+            },
+        },
+
+        {
+            action: NavigationAction.SelectPrevRoom,
+            keyCombo: {
+                key: Key.ARROW_UP,
+                altKey: true,
+            },
+        },
+        {
+            action: NavigationAction.SelectNextRoom,
+            keyCombo: {
+                key: Key.ARROW_DOWN,
+                altKey: true,
+            },
+        },
+        {
+            action: NavigationAction.SelectPrevUnreadRoom,
+            keyCombo: {
+                key: Key.ARROW_UP,
+                altKey: true,
+                shiftKey: true,
+            },
+        },
+        {
+            action: NavigationAction.SelectNextUnreadRoom,
+            keyCombo: {
+                key: Key.ARROW_DOWN,
+                altKey: true,
+                shiftKey: true,
+            },
+        },
+    ];
+}
+
+export const defaultBindingsProvider: IKeyBindingsProvider = {
+    getMessageComposerBindings: messageComposerBindings,
+    getAutocompleteBindings: autocompleteBindings,
+    getRoomListBindings: roomListBindings,
+    getRoomBindings: roomBindings,
+    getNavigationBindings: navigationBindings,
+}
diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts
new file mode 100644
index 0000000000..d862f10c02
--- /dev/null
+++ b/src/KeyBindingsManager.ts
@@ -0,0 +1,271 @@
+/*
+Copyright 2021 Clemens Zeidler
+
+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 { defaultBindingsProvider } from './KeyBindingsDefaults';
+import { isMac } from './Keyboard';
+
+/** Actions for the chat message composer component */
+export enum MessageComposerAction {
+    /** Send a message */
+    Send = 'Send',
+    /** Go backwards through the send history and use the message in composer view */
+    SelectPrevSendHistory = 'SelectPrevSendHistory',
+    /** Go forwards through the send history */
+    SelectNextSendHistory = 'SelectNextSendHistory',
+    /** Start editing the user's last sent message */
+    EditPrevMessage = 'EditPrevMessage',
+    /** Start editing the user's next sent message */
+    EditNextMessage = 'EditNextMessage',
+    /** Cancel editing a message or cancel replying to a message */
+    CancelEditing = 'CancelEditing',
+
+    /** Set bold format the current selection */
+    FormatBold = 'FormatBold',
+    /** Set italics format the current selection */
+    FormatItalics = 'FormatItalics',
+    /** Format the current selection as quote */
+    FormatQuote = 'FormatQuote',
+    /** Undo the last editing */
+    EditUndo = 'EditUndo',
+    /** Redo editing */
+    EditRedo = 'EditRedo',
+    /** Insert new line */
+    NewLine = 'NewLine',
+    /** Move the cursor to the start of the message */
+    MoveCursorToStart = 'MoveCursorToStart',
+    /** Move the cursor to the end of the message */
+    MoveCursorToEnd = 'MoveCursorToEnd',
+}
+
+/** Actions for text editing autocompletion */
+export enum AutocompleteAction {
+    /**
+     * Select previous selection or, if the autocompletion window is not shown, open the window and select the first
+     * selection.
+     */
+    CompleteOrPrevSelection = 'ApplySelection',
+    /** Select next selection or, if the autocompletion window is not shown, open it and select the first selection */
+    CompleteOrNextSelection = 'CompleteOrNextSelection',
+    /** Move to the previous autocomplete selection */
+    PrevSelection = 'PrevSelection',
+    /** Move to the next autocomplete selection */
+    NextSelection = 'NextSelection',
+    /** Close the autocompletion window */
+    Cancel = 'Cancel',
+}
+
+/** Actions for the room list sidebar */
+export enum RoomListAction {
+    /** Clear room list filter field */
+    ClearSearch = 'ClearSearch',
+    /** Navigate up/down in the room list */
+    PrevRoom = 'PrevRoom',
+    /** Navigate down in the room list */
+    NextRoom = 'NextRoom',
+    /** Select room from the room list */
+    SelectRoom = 'SelectRoom',
+    /** Collapse room list section */
+    CollapseSection = 'CollapseSection',
+    /** Expand room list section, if already expanded, jump to first room in the selection */
+    ExpandSection = 'ExpandSection',
+}
+
+/** Actions for the current room view */
+export enum RoomAction {
+    /** Scroll up in the timeline */
+    ScrollUp = 'ScrollUp',
+    /** Scroll down in the timeline */
+    RoomScrollDown = 'RoomScrollDown',
+    /** Dismiss read marker and jump to bottom */
+    DismissReadMarker = 'DismissReadMarker',
+    /** Jump to oldest unread message */
+    JumpToOldestUnread = 'JumpToOldestUnread',
+    /** Upload a file */
+    UploadFile = 'UploadFile',
+    /** Focus search message in a room (must be enabled) */
+    FocusSearch = 'FocusSearch',
+    /** Jump to the first (downloaded) message in the room */
+    JumpToFirstMessage = 'JumpToFirstMessage',
+    /** Jump to the latest message in the room */
+    JumpToLatestMessage = 'JumpToLatestMessage',
+}
+
+/** Actions for navigating do various menus, dialogs or screens */
+export enum NavigationAction {
+    /** Jump to room search (search for a room) */
+    FocusRoomSearch = 'FocusRoomSearch',
+    /** Toggle the room side panel */
+    ToggleRoomSidePanel = 'ToggleRoomSidePanel',
+    /** Toggle the user menu */
+    ToggleUserMenu = 'ToggleUserMenu',
+    /** Toggle the short cut help dialog */
+    ToggleShortCutDialog = 'ToggleShortCutDialog',
+    /** Got to the Element home screen */
+    GoToHome = 'GoToHome',
+    /** Select prev room */
+    SelectPrevRoom = 'SelectPrevRoom',
+    /** Select next room */
+    SelectNextRoom = 'SelectNextRoom',
+    /** Select prev room with unread messages */
+    SelectPrevUnreadRoom = 'SelectPrevUnreadRoom',
+    /** Select next room with unread messages */
+    SelectNextUnreadRoom = 'SelectNextUnreadRoom',
+}
+
+/**
+ * Represent a key combination.
+ *
+ * The combo is evaluated strictly, i.e. the KeyboardEvent must match exactly what is specified in the KeyCombo.
+ */
+export type KeyCombo = {
+    key?: string;
+
+    /** On PC: ctrl is pressed; on Mac: meta is pressed */
+    ctrlOrCmd?: boolean;
+
+    altKey?: boolean;
+    ctrlKey?: boolean;
+    metaKey?: boolean;
+    shiftKey?: boolean;
+}
+
+export type KeyBinding = {
+    action: T;
+    keyCombo: KeyCombo;
+}
+
+/**
+ * Helper method to check if a KeyboardEvent matches a KeyCombo
+ *
+ * Note, this method is only exported for testing.
+ */
+export function isKeyComboMatch(ev: KeyboardEvent | React.KeyboardEvent, combo: KeyCombo, onMac: boolean): boolean {
+    if (combo.key !== undefined) {
+        // When shift is pressed, letters are returned as upper case chars. In this case do a lower case comparison.
+        // This works for letter combos such as shift + U as well for none letter combos such as shift + Escape.
+        // If shift is not pressed, the toLowerCase conversion can be avoided.
+        if (ev.shiftKey) {
+            if (ev.key.toLowerCase() !== combo.key.toLowerCase()) {
+                return false;
+            }
+        } else if (ev.key !== combo.key) {
+            return false;
+        }
+    }
+
+    const comboCtrl = combo.ctrlKey ?? false;
+    const comboAlt = combo.altKey ?? false;
+    const comboShift = combo.shiftKey ?? false;
+    const comboMeta = combo.metaKey ?? false;
+    // Tests mock events may keep the modifiers undefined; convert them to booleans
+    const evCtrl = ev.ctrlKey ?? false;
+    const evAlt = ev.altKey ?? false;
+    const evShift = ev.shiftKey ?? false;
+    const evMeta = ev.metaKey ?? false;
+    // When ctrlOrCmd is set, the keys need do evaluated differently on PC and Mac
+    if (combo.ctrlOrCmd) {
+        if (onMac) {
+            if (!evMeta
+                || evCtrl !== comboCtrl
+                || evAlt !== comboAlt
+                || evShift !== comboShift) {
+                return false;
+            }
+        } else {
+            if (!evCtrl
+                || evMeta !== comboMeta
+                || evAlt !== comboAlt
+                || evShift !== comboShift) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    if (evMeta !== comboMeta
+        || evCtrl !== comboCtrl
+        || evAlt !== comboAlt
+        || evShift !== comboShift) {
+        return false;
+    }
+
+    return true;
+}
+
+export type KeyBindingGetter = () => KeyBinding[];
+
+export interface IKeyBindingsProvider {
+    getMessageComposerBindings: KeyBindingGetter;
+    getAutocompleteBindings: KeyBindingGetter;
+    getRoomListBindings: KeyBindingGetter;
+    getRoomBindings: KeyBindingGetter;
+    getNavigationBindings: KeyBindingGetter;
+}
+
+export class KeyBindingsManager {
+    /**
+     * List of key bindings providers.
+     *
+     * Key bindings from the first provider(s) in the list will have precedence over key bindings from later providers.
+     *
+     * To overwrite the default key bindings add a new providers before the default provider, e.g. a provider for
+     * customized key bindings.
+     */
+    bindingsProviders: IKeyBindingsProvider[] = [
+        defaultBindingsProvider,
+    ];
+
+    /**
+     * Finds a matching KeyAction for a given KeyboardEvent
+     */
+    private getAction(getters: KeyBindingGetter[], ev: KeyboardEvent | React.KeyboardEvent)
+        : T | undefined {
+        for (const getter of getters) {
+            const bindings = getter();
+            const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac));
+            if (binding) {
+                return binding.action;
+            }
+        }
+        return undefined;
+    }
+
+    getMessageComposerAction(ev: KeyboardEvent | React.KeyboardEvent): MessageComposerAction | undefined {
+        return this.getAction(this.bindingsProviders.map(it => it.getMessageComposerBindings), ev);
+    }
+
+    getAutocompleteAction(ev: KeyboardEvent | React.KeyboardEvent): AutocompleteAction | undefined {
+        return this.getAction(this.bindingsProviders.map(it => it.getAutocompleteBindings), ev);
+    }
+
+    getRoomListAction(ev: KeyboardEvent | React.KeyboardEvent): RoomListAction | undefined {
+        return this.getAction(this.bindingsProviders.map(it => it.getRoomListBindings), ev);
+    }
+
+    getRoomAction(ev: KeyboardEvent | React.KeyboardEvent): RoomAction | undefined {
+        return this.getAction(this.bindingsProviders.map(it => it.getRoomBindings), ev);
+    }
+
+    getNavigationAction(ev: KeyboardEvent | React.KeyboardEvent): NavigationAction | undefined {
+        return this.getAction(this.bindingsProviders.map(it => it.getNavigationBindings), ev);
+    }
+}
+
+const manager = new KeyBindingsManager();
+
+export function getKeyBindingsManager(): KeyBindingsManager {
+    return manager;
+}
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index f87af1a791..b0a1292ba1 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -17,8 +17,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
-import Matrix from 'matrix-js-sdk';
+import { createClient } from 'matrix-js-sdk/src/matrix';
 import { InvalidStoreError } from "matrix-js-sdk/src/errors";
 import { MatrixClient } from "matrix-js-sdk/src/client";
 import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
@@ -46,11 +45,13 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
 import {Mjolnir} from "./mjolnir/Mjolnir";
 import DeviceListener from "./DeviceListener";
 import {Jitsi} from "./widgets/Jitsi";
-import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
+import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY} from "./BasePlatform";
 import ThreepidInviteStore from "./stores/ThreepidInviteStore";
 import CountlyAnalytics from "./CountlyAnalytics";
 import CallHandler from './CallHandler';
 import LifecycleCustomisations from "./customisations/Lifecycle";
+import ErrorDialog from "./components/views/dialogs/ErrorDialog";
+import {_t} from "./languageHandler";
 
 const HOMESERVER_URL_KEY = "mx_hs_url";
 const ID_SERVER_URL_KEY = "mx_is_url";
@@ -162,7 +163,8 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> {
  *     query-parameters extracted from the real query-string of the starting
  *     URI.
  *
- * @param {String} defaultDeviceDisplayName
+ * @param {string} defaultDeviceDisplayName
+ * @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
  *
  * @returns {Promise} promise which resolves to true if we completed the token
  *    login, else false
@@ -170,6 +172,7 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> {
 export function attemptTokenLogin(
     queryParams: Record,
     defaultDeviceDisplayName?: string,
+    fragmentAfterLogin?: string,
 ): Promise {
     if (!queryParams.loginToken) {
         return Promise.resolve(false);
@@ -179,6 +182,12 @@ export function attemptTokenLogin(
     const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY);
     if (!homeserver) {
         console.warn("Cannot log in with token: can't determine HS URL to use");
+        Modal.createTrackedDialog("SSO", "Unknown HS", ErrorDialog, {
+            title: _t("We couldn't log you in"),
+            description: _t("We asked the browser to remember which homeserver you use to let you sign in, " +
+                "but unfortunately your browser has forgotten it. Go to the sign in page and try again."),
+            button: _t("Try again"),
+        });
         return Promise.resolve(false);
     }
 
@@ -198,8 +207,28 @@ export function attemptTokenLogin(
             return true;
         });
     }).catch((err) => {
-        console.error("Failed to log in with login token: " + err + " " +
-                      err.data);
+        Modal.createTrackedDialog("SSO", "Token Rejected", ErrorDialog, {
+            title: _t("We couldn't log you in"),
+            description: err.name === "ConnectionError"
+                ? _t("Your homeserver was unreachable and was not able to log you in. Please try again. " +
+                    "If this continues, please contact your homeserver administrator.")
+                : _t("Your homeserver rejected your log in attempt. " +
+                    "This could be due to things just taking too long. Please try again. " +
+                    "If this continues, please contact your homeserver administrator."),
+            button: _t("Try again"),
+            onFinished: tryAgain => {
+                if (tryAgain) {
+                    const cli = createClient({
+                        baseUrl: homeserver,
+                        idBaseUrl: identityServer,
+                    });
+                    const idpId = localStorage.getItem(SSO_IDP_ID_KEY) || undefined;
+                    PlatformPeg.get().startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId);
+                }
+            },
+        });
+        console.error("Failed to log in with login token:");
+        console.error(err);
         return false;
     });
 }
@@ -246,7 +275,7 @@ function registerAsGuest(
     console.log(`Doing guest login on ${hsUrl}`);
 
     // create a temporary MatrixClient to do the login
-    const client = Matrix.createClient({
+    const client = createClient({
         baseUrl: hsUrl,
     });
 
@@ -366,7 +395,7 @@ async function abortLogin() {
 //      The plan is to gradually move the localStorage access done here into
 //      SessionStore to avoid bugs where the view becomes out-of-sync with
 //      localStorage (e.g. isGuest etc.)
-async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise {
+export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise {
     const ignoreGuest = opts?.ignoreGuest;
 
     if (!localStorage) {
diff --git a/src/Livestream.ts b/src/Livestream.ts
new file mode 100644
index 0000000000..2389132762
--- /dev/null
+++ b/src/Livestream.ts
@@ -0,0 +1,55 @@
+/*
+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 { ClientWidgetApi } from "matrix-widget-api";
+import { MatrixClientPeg } from "./MatrixClientPeg";
+import SdkConfig from "./SdkConfig";
+import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
+
+export function getConfigLivestreamUrl() {
+    return SdkConfig.get()["audioStreamUrl"];
+}
+
+// Dummy rtmp URL used to signal that we want a special audio-only stream
+const AUDIOSTREAM_DUMMY_URL = 'rtmp://audiostream.dummy/';
+
+async function createLiveStream(roomId: string) {
+    const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
+
+    const url = getConfigLivestreamUrl() + "/createStream";
+
+    const response = await window.fetch(url, {
+        method: 'POST',
+        headers: {
+            "Content-Type": "application/json",
+        },
+        body: JSON.stringify({
+            room_id: roomId,
+            openid_token: openIdToken,
+        }),
+    });
+
+    const respBody = await response.json();
+    return respBody['stream_id'];
+}
+
+export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) {
+    const streamId = await createLiveStream(roomId);
+
+    await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, {
+        rtmpStreamKey: AUDIOSTREAM_DUMMY_URL + streamId,
+    });
+}
diff --git a/src/Login.ts b/src/Login.ts
index 6493b244e0..db3c4c11e4 100644
--- a/src/Login.ts
+++ b/src/Login.ts
@@ -19,7 +19,7 @@ limitations under the License.
 */
 
 // @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
-import Matrix from "matrix-js-sdk";
+import {createClient} from "matrix-js-sdk/src/matrix";
 import { MatrixClient } from "matrix-js-sdk/src/client";
 import { IMatrixClientCreds } from "./MatrixClientPeg";
 import SecurityCustomisations from "./customisations/Security";
@@ -33,10 +33,20 @@ interface IPasswordFlow {
     type: "m.login.password";
 }
 
+export enum IdentityProviderBrand {
+    Gitlab = "org.matrix.gitlab",
+    Github = "org.matrix.github",
+    Apple = "org.matrix.apple",
+    Google = "org.matrix.google",
+    Facebook = "org.matrix.facebook",
+    Twitter = "org.matrix.twitter",
+}
+
 export interface IIdentityProvider {
     id: string;
     name: string;
     icon?: string;
+    brand?: IdentityProviderBrand | string;
 }
 
 export interface ISSOFlow {
@@ -105,7 +115,7 @@ export default class Login {
      */
     public createTemporaryClient(): MatrixClient {
         if (this.tempClient) return this.tempClient; // use memoization
-        return this.tempClient = Matrix.createClient({
+        return this.tempClient = createClient({
             baseUrl: this.hsUrl,
             idBaseUrl: this.isUrl,
         });
@@ -200,7 +210,7 @@ export async function sendLoginRequest(
     loginType: string,
     loginParams: ILoginParams,
 ): Promise {
-    const client = Matrix.createClient({
+    const client = createClient({
         baseUrl: hsUrl,
         idBaseUrl: isUrl,
     });
diff --git a/src/Markdown.js b/src/Markdown.js
index dc4d442aff..f670bded12 100644
--- a/src/Markdown.js
+++ b/src/Markdown.js
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import commonmark from 'commonmark';
+import * as commonmark from 'commonmark';
 import {escape} from "lodash";
 
 const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index bb4be663b6..7db5ed1a4e 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -261,7 +261,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
     }
 
     public getHomeserverName(): string {
-        const matches = /^@.+:(.+)$/.exec(this.matrixClient.credentials.userId);
+        const matches = /^@[^:]+:(.+)$/.exec(this.matrixClient.credentials.userId);
         if (matches === null || matches.length < 1) {
             throw new Error("Failed to derive homeserver name from user ID!");
         }
@@ -279,6 +279,10 @@ class _MatrixClientPeg implements IMatrixClientPeg {
             timelineSupport: true,
             forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'),
             fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
+            // Gather up to 20 ICE candidates when a call arrives: this should be more than we'd
+            // ever normally need, so effectively this should make all the gathering happen when
+            // the call arrives.
+            iceCandidatePoolSize: 20,
             verificationMethods: [
                 verificationMethods.SAS,
                 SHOW_QR_CODE_METHOD,
@@ -292,10 +296,11 @@ class _MatrixClientPeg implements IMatrixClientPeg {
         // These are always installed regardless of the labs flag so that
         // cross-signing features can toggle on without reloading and also be
         // accessed immediately after login.
-        const customisedCallbacks = {
-            getDehydrationKey: SecurityCustomisations.getDehydrationKey,
-        };
-        Object.assign(opts.cryptoCallbacks, crossSigningCallbacks, customisedCallbacks);
+        Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
+        if (SecurityCustomisations.getDehydrationKey) {
+            opts.cryptoCallbacks.getDehydrationKey =
+                SecurityCustomisations.getDehydrationKey;
+        }
 
         this.matrixClient = createMatrixClient(opts);
 
diff --git a/src/Notifier.ts b/src/Notifier.ts
index 6460be20ad..f68bfabc18 100644
--- a/src/Notifier.ts
+++ b/src/Notifier.ts
@@ -36,6 +36,7 @@ import {SettingLevel} from "./settings/SettingLevel";
 import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
 import RoomViewStore from "./stores/RoomViewStore";
 import UserActivity from "./UserActivity";
+import {mediaFromMxc} from "./customisations/Media";
 
 /*
  * Dispatches:
@@ -150,7 +151,7 @@ export const Notifier = {
         // Ideally in here we could use MSC1310 to detect the type of file, and reject it.
 
         return {
-            url: MatrixClientPeg.get().mxcUrlToHttp(content.url),
+            url: mediaFromMxc(content.url).srcHttp,
             name: content.name,
             type: content.type,
             size: content.size,
diff --git a/src/ObjectUtils.js b/src/ObjectUtils.js
deleted file mode 100644
index 24dfe61d68..0000000000
--- a/src/ObjectUtils.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-/**
- * For two objects of the form { key: [val1, val2, val3] }, work out the added/removed
- * values. Entirely new keys will result in the entire value array being added.
- * @param {Object} before
- * @param {Object} after
- * @return {Object[]} An array of objects with the form:
- * { key: $KEY, val: $VALUE, place: "add|del" }
- */
-export function getKeyValueArrayDiffs(before, after) {
-    const results = [];
-    const delta = {};
-    Object.keys(before).forEach(function(beforeKey) {
-        delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially
-        delta[beforeKey]--; // keys present in the past have -ve values
-    });
-    Object.keys(after).forEach(function(afterKey) {
-        delta[afterKey] = delta[afterKey] || 0; // init to 0 initially
-        delta[afterKey]++; // keys present in the future have +ve values
-    });
-
-    Object.keys(delta).forEach(function(muxedKey) {
-        switch (delta[muxedKey]) {
-            case 1: // A new key in after
-                after[muxedKey].forEach(function(afterVal) {
-                    results.push({ place: "add", key: muxedKey, val: afterVal });
-                });
-                break;
-            case -1: // A before key was removed
-                before[muxedKey].forEach(function(beforeVal) {
-                    results.push({ place: "del", key: muxedKey, val: beforeVal });
-                });
-                break;
-            case 0: {// A mix of added/removed keys
-                // compare old & new vals
-                const itemDelta = {};
-                before[muxedKey].forEach(function(beforeVal) {
-                    itemDelta[beforeVal] = itemDelta[beforeVal] || 0;
-                    itemDelta[beforeVal]--;
-                });
-                after[muxedKey].forEach(function(afterVal) {
-                    itemDelta[afterVal] = itemDelta[afterVal] || 0;
-                    itemDelta[afterVal]++;
-                });
-
-                Object.keys(itemDelta).forEach(function(item) {
-                    if (itemDelta[item] === 1) {
-                        results.push({ place: "add", key: muxedKey, val: item });
-                    } else if (itemDelta[item] === -1) {
-                        results.push({ place: "del", key: muxedKey, val: item });
-                    } else {
-                        // itemDelta of 0 means it was unchanged between before/after
-                    }
-                });
-                break;
-            }
-            default:
-                console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!");
-                break;
-        }
-    });
-
-    return results;
-}
-
-/**
- * Shallow-compare two objects for equality: each key and value must be identical
- * @param {Object} objA First object to compare against the second
- * @param {Object} objB Second object to compare against the first
- * @return {boolean} whether the two objects have same key=values
- */
-export function shallowEqual(objA, objB) {
-    if (objA === objB) {
-        return true;
-    }
-
-    if (typeof objA !== 'object' || objA === null ||
-          typeof objB !== 'object' || objB === null) {
-        return false;
-    }
-
-    const keysA = Object.keys(objA);
-    const keysB = Object.keys(objB);
-
-    if (keysA.length !== keysB.length) {
-        return false;
-    }
-
-    for (let i = 0; i < keysA.length; i++) {
-        const key = keysA[i];
-        if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
-            return false;
-        }
-    }
-
-    return true;
-}
diff --git a/src/PasswordReset.js b/src/PasswordReset.js
index b38a9de960..6fe6ca82cc 100644
--- a/src/PasswordReset.js
+++ b/src/PasswordReset.js
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import * as Matrix from 'matrix-js-sdk';
+import { createClient } from 'matrix-js-sdk/src/matrix';
 import { _t } from './languageHandler';
 
 /**
@@ -32,7 +32,7 @@ export default class PasswordReset {
      * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
      */
     constructor(homeserverUrl, identityUrl) {
-        this.client = Matrix.createClient({
+        this.client = createClient({
             baseUrl: homeserverUrl,
             idBaseUrl: identityUrl,
         });
diff --git a/src/PhasedRollOut.js b/src/PhasedRollOut.js
deleted file mode 100644
index b17ed37974..0000000000
--- a/src/PhasedRollOut.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import SdkConfig from './SdkConfig';
-import {hashCode} from './utils/FormattingUtils';
-
-export function phasedRollOutExpiredForUser(username, feature, now, rollOutConfig = SdkConfig.get().phasedRollOut) {
-    if (!rollOutConfig) {
-        console.log(`no phased rollout configuration, so enabling ${feature}`);
-        return true;
-    }
-    const featureConfig = rollOutConfig[feature];
-    if (!featureConfig) {
-        console.log(`${feature} doesn't have phased rollout configured, so enabling`);
-        return true;
-    }
-    if (!Number.isFinite(featureConfig.offset) || !Number.isFinite(featureConfig.period)) {
-        console.error(`phased rollout of ${feature} is misconfigured, ` +
-            `offset and/or period are not numbers, so disabling`, featureConfig);
-        return false;
-    }
-
-    const hash = hashCode(username);
-    //ms -> min, enable users at minute granularity
-    const bucketRatio = 1000 * 60;
-    const bucketCount = featureConfig.period / bucketRatio;
-    const userBucket = hash % bucketCount;
-    const userMs = userBucket * bucketRatio;
-    const enableAt = featureConfig.offset + userMs;
-    const result = now >= enableAt;
-    const bucketStr = `(bucket ${userBucket}/${bucketCount})`;
-    if (result) {
-        console.log(`${feature} enabled for ${username} ${bucketStr}`);
-    } else {
-        console.log(`${feature} will be enabled for ${username} in ${Math.ceil((enableAt - now)/1000)}s ${bucketStr}`);
-    }
-    return result;
-}
diff --git a/src/Presence.ts b/src/Presence.ts
index 660bb0ac94..eb56c5714e 100644
--- a/src/Presence.ts
+++ b/src/Presence.ts
@@ -99,9 +99,9 @@ class Presence {
 
         try {
             await MatrixClientPeg.get().setPresence(this.state);
-            console.info("Presence: %s", newState);
+            console.info("Presence:", newState);
         } catch (err) {
-            console.error("Failed to set presence: %s", err);
+            console.error("Failed to set presence:", err);
             this.state = oldState;
         }
     }
diff --git a/src/Resend.js b/src/Resend.js
index 5638313306..bf69e59c1a 100644
--- a/src/Resend.js
+++ b/src/Resend.js
@@ -17,7 +17,7 @@ limitations under the License.
 
 import {MatrixClientPeg} from './MatrixClientPeg';
 import dis from './dispatcher/dispatcher';
-import { EventStatus } from 'matrix-js-sdk';
+import { EventStatus } from 'matrix-js-sdk/src/models/event';
 
 export default class Resend {
     static resendUnsentEvents(room) {
diff --git a/src/RoomInvite.js b/src/RoomInvite.js
index 06d3fb04e8..aa758ecbdc 100644
--- a/src/RoomInvite.js
+++ b/src/RoomInvite.js
@@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter';
 import Modal from './Modal';
 import * as sdk from './';
 import { _t } from './languageHandler';
-import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
+import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
 import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
 import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
 
@@ -49,11 +49,14 @@ export function showStartChatInviteDialog(initialText) {
     );
 }
 
-export function showRoomInviteDialog(roomId) {
+export function showRoomInviteDialog(roomId, initialText = "") {
     // This dialog handles the room creation internally - we don't need to worry about it.
-    const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
     Modal.createTrackedDialog(
-        'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
+        "Invite Users", "", InviteDialog, {
+            kind: KIND_INVITE,
+            initialText,
+            roomId,
+        },
         /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
     );
 }
diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js
index a86c521ac4..600655f635 100644
--- a/src/RoomNotifs.js
+++ b/src/RoomNotifs.js
@@ -202,12 +202,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
 }
 
 function findOverrideMuteRule(roomId) {
-    if (!MatrixClientPeg.get().pushRules ||
-        !MatrixClientPeg.get().pushRules['global'] ||
-        !MatrixClientPeg.get().pushRules['global'].override) {
+    const cli = MatrixClientPeg.get();
+    if (!cli.pushRules ||
+        !cli.pushRules['global'] ||
+        !cli.pushRules['global'].override) {
         return null;
     }
-    for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
+    for (const rule of cli.pushRules['global'].override) {
         if (isRuleForRoom(roomId, rule)) {
             if (isMuteRule(rule) && rule.enabled) {
                 return rule;
diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js
index 1ea9d39e2f..200b4fd7b9 100644
--- a/src/ScalarAuthClient.js
+++ b/src/ScalarAuthClient.js
@@ -21,9 +21,9 @@ import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
 import {MatrixClientPeg} from "./MatrixClientPeg";
 import request from "browser-request";
 
-import * as Matrix from 'matrix-js-sdk';
 import SdkConfig from "./SdkConfig";
 import {WidgetType} from "./widgets/WidgetType";
+import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
 
 // The version of the integration manager API we're intending to work with
 const imApiVersion = "1.1";
@@ -153,7 +153,7 @@ export default class ScalarAuthClient {
                 parsedImRestUrl.path = '';
                 parsedImRestUrl.pathname = '';
                 return startTermsFlow([new Service(
-                    Matrix.SERVICE_TYPES.IM,
+                    SERVICE_TYPES.IM,
                     parsedImRestUrl.format(),
                     token,
                 )], this.termsInteractionCallback).then(() => {
diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js
index 896e27d92c..3f75b3788c 100644
--- a/src/ScalarMessaging.js
+++ b/src/ScalarMessaging.js
@@ -237,7 +237,7 @@ Example:
 */
 
 import {MatrixClientPeg} from './MatrixClientPeg';
-import { MatrixEvent } from 'matrix-js-sdk';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
 import dis from './dispatcher/dispatcher';
 import WidgetUtils from './utils/WidgetUtils';
 import RoomViewStore from './stores/RoomViewStore';
diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts
index 220320470a..203830d232 100644
--- a/src/SecurityManager.ts
+++ b/src/SecurityManager.ts
@@ -98,11 +98,27 @@ async function getSecretStorageKey(
     { keys: keyInfos }: { keys: Record },
     ssssItemName,
 ): Promise<[string, Uint8Array]> {
-    const keyInfoEntries = Object.entries(keyInfos);
-    if (keyInfoEntries.length > 1) {
-        throw new Error("Multiple storage key requests not implemented");
+    const cli = MatrixClientPeg.get();
+    let keyId = await cli.getDefaultSecretStorageKeyId();
+    let keyInfo;
+    if (keyId) {
+        // use the default SSSS key if set
+        keyInfo = keyInfos[keyId];
+        if (!keyInfo) {
+            // if the default key is not available, pretend the default key
+            // isn't set
+            keyId = undefined;
+        }
+    }
+    if (!keyId) {
+        // if no default SSSS key is set, fall back to a heuristic of using the
+        // only available key, if only one key is set
+        const keyInfoEntries = Object.entries(keyInfos);
+        if (keyInfoEntries.length > 1) {
+            throw new Error("Multiple storage key requests not implemented");
+        }
+        [keyId, keyInfo] = keyInfoEntries[0];
     }
-    const [keyId, keyInfo] = keyInfoEntries[0];
 
     // Check the in-memory cache
     if (isCachingAllowed() && secretStorageKeys[keyId]) {
@@ -379,6 +395,8 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
     } catch (e) {
         SecurityCustomisations.catchAccessSecretStorageError?.(e);
         console.error(e);
+        // Re-throw so that higher level logic can abort as needed
+        throw e;
     } finally {
         // Clear secret storage key cache now that work is complete
         secretStorageBeingAccessed = false;
diff --git a/src/Skinner.js b/src/Skinner.js
index d17bc1782a..ef340e4052 100644
--- a/src/Skinner.js
+++ b/src/Skinner.js
@@ -23,7 +23,7 @@ class Skinner {
         if (!name) throw new Error(`Invalid component name: ${name}`);
         if (this.components === null) {
             throw new Error(
-                "Attempted to get a component before a skin has been loaded."+
+                `Attempted to get a component (${name}) before a skin has been loaded.`+
                 " This is probably because either:"+
                 " a) Your app has not called sdk.loadSkin(), or"+
                 " b) A component has called getComponent at the root level",
diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 79c21c4af5..3b6a202cf6 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -20,6 +20,7 @@ limitations under the License.
 
 import * as React from 'react';
 
+import { ContentHelpers } from 'matrix-js-sdk';
 import {MatrixClientPeg} from './MatrixClientPeg';
 import dis from './dispatcher/dispatcher';
 import * as sdk from './index';
@@ -48,6 +49,7 @@ import SettingsStore from "./settings/SettingsStore";
 import {UIFeature} from "./settings/UIFeature";
 import {CHAT_EFFECTS} from "./effects"
 import CallHandler from "./CallHandler";
+import {guessAndSetDMRoom} from "./Rooms";
 
 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
 interface HTMLInputEvent extends Event {
@@ -125,10 +127,10 @@ export class Command {
         return this.getCommand() + " " + this.args;
     }
 
-    run(roomId: string, args: string, cmd: string) {
+    run(roomId: string, args: string) {
         // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
         if (!this.runFn) return reject(_t("Command error"));
-        return this.runFn.bind(this)(roomId, args, cmd);
+        return this.runFn.bind(this)(roomId, args);
     }
 
     getUsage() {
@@ -153,6 +155,18 @@ function success(promise?: Promise) {
  */
 
 export const Commands = [
+    new Command({
+        command: 'spoiler',
+        args: '',
+        description: _td('Sends the given message as a spoiler'),
+        runFn: function(roomId, message) {
+            return success(ContentHelpers.makeHtmlMessage(
+                message,
+                `${message}`,
+            ));
+        },
+        category: CommandCategories.messages,
+    }),
     new Command({
         command: 'shrug',
         args: '',
@@ -162,7 +176,7 @@ export const Commands = [
             if (args) {
                 message = message + ' ' + args;
             }
-            return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
+            return success(ContentHelpers.makeTextMessage(message));
         },
         category: CommandCategories.messages,
     }),
@@ -175,7 +189,7 @@ export const Commands = [
             if (args) {
                 message = message + ' ' + args;
             }
-            return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
+            return success(ContentHelpers.makeTextMessage(message));
         },
         category: CommandCategories.messages,
     }),
@@ -188,7 +202,7 @@ export const Commands = [
             if (args) {
                 message = message + ' ' + args;
             }
-            return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
+            return success(ContentHelpers.makeTextMessage(message));
         },
         category: CommandCategories.messages,
     }),
@@ -201,7 +215,7 @@ export const Commands = [
             if (args) {
                 message = message + ' ' + args;
             }
-            return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
+            return success(ContentHelpers.makeTextMessage(message));
         },
         category: CommandCategories.messages,
     }),
@@ -210,7 +224,7 @@ export const Commands = [
         args: '',
         description: _td('Sends a message as plain text, without interpreting it as markdown'),
         runFn: function(roomId, messages) {
-            return success(MatrixClientPeg.get().sendTextMessage(roomId, messages));
+            return success(ContentHelpers.makeTextMessage(messages));
         },
         category: CommandCategories.messages,
     }),
@@ -219,7 +233,7 @@ export const Commands = [
         args: '',
         description: _td('Sends a message as html, without interpreting it as markdown'),
         runFn: function(roomId, messages) {
-            return success(MatrixClientPeg.get().sendHtmlMessage(roomId, messages, messages));
+            return success(ContentHelpers.makeHtmlMessage(messages, messages));
         },
         category: CommandCategories.messages,
     }),
@@ -440,15 +454,14 @@ export const Commands = [
     }),
     new Command({
         command: 'invite',
-        args: '',
+        args: ' []',
         description: _td('Invites user with given id to current room'),
         runFn: function(roomId, args) {
             if (args) {
-                const matches = args.match(/^(\S+)$/);
-                if (matches) {
+                const [address, reason] = args.split(/\s+(.+)/);
+                if (address) {
                     // We use a MultiInviter to re-use the invite logic, even though
                     // we're only inviting one user.
-                    const address = matches[1];
                     // If we need an identity server but don't have one, things
                     // get a bit more complex here, but we try to show something
                     // meaningful.
@@ -489,7 +502,7 @@ export const Commands = [
                     }
                     const inviter = new MultiInviter(roomId);
                     return success(prom.then(() => {
-                        return inviter.invite([address]);
+                        return inviter.invite([address], reason);
                     }).then(() => {
                         if (inviter.getCompletionState(address) !== "invited") {
                             throw new Error(inviter.getErrorText(address));
@@ -965,7 +978,7 @@ export const Commands = [
         args: '',
         runFn: function(roomId, args) {
             if (!args) return reject(this.getUserId());
-            return success(MatrixClientPeg.get().sendHtmlMessage(roomId, args, textToHtmlRainbow(args)));
+            return success(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
         },
         category: CommandCategories.messages,
     }),
@@ -975,7 +988,7 @@ export const Commands = [
         args: '',
         runFn: function(roomId, args) {
             if (!args) return reject(this.getUserId());
-            return success(MatrixClientPeg.get().sendHtmlEmote(roomId, args, textToHtmlRainbow(args)));
+            return success(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
         },
         category: CommandCategories.messages,
     }),
@@ -1039,9 +1052,7 @@ export const Commands = [
 
             return success((async () => {
                 if (isPhoneNumber) {
-                    const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', {
-                        'm.id.phone': userId,
-                    });
+                    const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
                     if (!results || results.length === 0 || !results[0].userid) {
                         throw new Error("Unable to find Matrix ID for phone number");
                     }
@@ -1112,6 +1123,24 @@ export const Commands = [
             return success();
         },
     }),
+    new Command({
+        command: "converttodm",
+        description: _td("Converts the room to a DM"),
+        category: CommandCategories.other,
+        runFn: function(roomId, args) {
+            const room = MatrixClientPeg.get().getRoom(roomId);
+            return success(guessAndSetDMRoom(room, true));
+        },
+    }),
+    new Command({
+        command: "converttoroom",
+        description: _td("Converts the DM to a room"),
+        category: CommandCategories.other,
+        runFn: function(roomId, args) {
+            const room = MatrixClientPeg.get().getRoom(roomId);
+            return success(guessAndSetDMRoom(room, false));
+        },
+    }),
 
     // Command definitions for autocompletion ONLY:
     // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
@@ -1163,7 +1192,7 @@ export function parseCommandString(input: string) {
     input = input.replace(/\s+$/, '');
     if (input[0] !== '/') return {}; // not a command
 
-    const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/);
+    const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/);
     let cmd;
     let args;
     if (bits) {
@@ -1184,10 +1213,13 @@ export function parseCommandString(input: string) {
  * processing the command, or 'promise' if a request was sent out.
  * Returns null if the input didn't match a command.
  */
-export function getCommand(roomId: string, input: string) {
+export function getCommand(input: string) {
     const {cmd, args} = parseCommandString(input);
 
     if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) {
-        return () => CommandMap.get(cmd).run(roomId, args, cmd);
+        return {
+            cmd: CommandMap.get(cmd),
+            args,
+        };
     }
 }
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 56e9abc0f2..3afe41d216 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -19,6 +19,7 @@ import * as Roles from './Roles';
 import {isValid3pidInvite} from "./RoomInvite";
 import SettingsStore from "./settings/SettingsStore";
 import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
+import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
 
 function textForMemberEvent(ev) {
     // XXX: SYJS-16 "sender is sometimes null for join messages"
@@ -477,6 +478,11 @@ function textForWidgetEvent(event) {
     }
 }
 
+function textForWidgetLayoutEvent(event) {
+    const senderName = event.sender?.name || event.getSender();
+    return _t("%(senderName)s has updated the widget layout", {senderName});
+}
+
 function textForMjolnirEvent(event) {
     const senderName = event.getSender();
     const {entity: prevEntity} = event.getPrevContent();
@@ -583,6 +589,7 @@ const stateHandlers = {
 
     // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
     'im.vector.modular.widgets': textForWidgetEvent,
+    [WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
 };
 
 // Add all the Mjolnir stuff to the renderer
diff --git a/src/Velociraptor.js b/src/Velociraptor.js
index ce52f60dbd..2da54babe5 100644
--- a/src/Velociraptor.js
+++ b/src/Velociraptor.js
@@ -118,25 +118,10 @@ export default class Velociraptor extends React.Component {
                     domNode.style.visibility = restingStyle.visibility;
                 });
 
-            /*
-            console.log("enter:",
-                        JSON.stringify(transitionOpts[i-1]),
-                        "->",
-                        JSON.stringify(restingStyle));
-            */
-        } else if (node === null) {
-            // Velocity stores data on elements using the jQuery .data()
-            // method, and assumes you'll be using jQuery's .remove() to
-            // remove the element, but we don't use jQuery, so we need to
-            // blow away the element's data explicitly otherwise it will leak.
-            // This uses Velocity's internal jQuery compatible wrapper.
-            // See the bug at
-            // https://github.com/julianshapiro/velocity/issues/300
-            // and the FAQ entry, "Preventing memory leaks when
-            // creating/destroying large numbers of elements"
-            // (https://github.com/julianshapiro/velocity/issues/47)
-            const domNode = ReactDom.findDOMNode(this.nodes[k]);
-            if (domNode) Velocity.Utilities.removeData(domNode);
+            // console.log("enter:",
+            //             JSON.stringify(transitionOpts[i-1]),
+            //             "->",
+            //             JSON.stringify(restingStyle));
         }
         this.nodes[k] = node;
     }
diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts
new file mode 100644
index 0000000000..4f5613b4a8
--- /dev/null
+++ b/src/VoipUserMapper.ts
@@ -0,0 +1,112 @@
+/*
+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 { ensureVirtualRoomExists, findDMForUser } from './createRoom';
+import { MatrixClientPeg } from "./MatrixClientPeg";
+import DMRoomMap from "./utils/DMRoomMap";
+import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler';
+import { Room } from 'matrix-js-sdk/src/models/room';
+
+// Functions for mapping virtual users & rooms. Currently the only lookup
+// is sip virtual: there could be others in the future.
+
+export default class VoipUserMapper {
+    private virtualRoomIdCache = new Set();
+
+    public static sharedInstance(): VoipUserMapper {
+        if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper();
+        return window.mxVoipUserMapper;
+    }
+
+    private async userToVirtualUser(userId: string): Promise {
+        const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
+        if (results.length === 0) return null;
+        return results[0].userid;
+    }
+
+    public async getOrCreateVirtualRoomForRoom(roomId: string): Promise {
+        const userId = DMRoomMap.shared().getUserIdForRoomId(roomId);
+        if (!userId) return null;
+
+        const virtualUser = await this.userToVirtualUser(userId);
+        if (!virtualUser) return null;
+
+        const virtualRoomId = await ensureVirtualRoomExists(MatrixClientPeg.get(), virtualUser, roomId);
+        MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, {
+            native_room: roomId,
+        });
+
+        return virtualRoomId;
+    }
+
+    public nativeRoomForVirtualRoom(roomId: string): string {
+        const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
+        if (!virtualRoom) return null;
+        const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
+        if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null;
+        return virtualRoomEvent.getContent()['native_room'] || null;
+    }
+
+    public isVirtualRoom(room: Room): boolean {
+        if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
+
+        if (this.virtualRoomIdCache.has(room.roomId)) return true;
+
+        // also look in the create event for the claimed native room ID, which is the only
+        // way we can recognise a virtual room we've created when it first arrives down
+        // our stream. We don't trust this in general though, as it could be faked by an
+        // inviter: our main source of truth is the DM state.
+        const roomCreateEvent = room.currentState.getStateEvents("m.room.create", "");
+        if (!roomCreateEvent || !roomCreateEvent.getContent()) return false;
+        // we only look at this for rooms we created (so inviters can't just cause rooms
+        // to be invisible)
+        if (roomCreateEvent.getSender() !== MatrixClientPeg.get().getUserId()) return false;
+        const claimedNativeRoomId = roomCreateEvent.getContent()[VIRTUAL_ROOM_EVENT_TYPE];
+        return Boolean(claimedNativeRoomId);
+    }
+
+    public async onNewInvitedRoom(invitedRoom: Room) {
+        if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
+
+        const inviterId = invitedRoom.getDMInviter();
+        console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
+        const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
+        if (result.length === 0) {
+            return true;
+        }
+
+        if (result[0].fields.is_virtual) {
+            const nativeUser = result[0].userid;
+            const nativeRoom = findDMForUser(MatrixClientPeg.get(), nativeUser);
+            if (nativeRoom) {
+                // It's a virtual room with a matching native room, so set the room account data. This
+                // will make sure we know where how to map calls and also allow us know not to display
+                // it in the future.
+                MatrixClientPeg.get().setRoomAccountData(invitedRoom.roomId, VIRTUAL_ROOM_EVENT_TYPE, {
+                    native_room: nativeRoom.roomId,
+                });
+                // also auto-join the virtual room if we have a matching native room
+                // (possibly we should only join if we've also joined the native room, then we'd also have
+                // to make sure we joined virtual rooms on joining a native one)
+                MatrixClientPeg.get().joinRoom(invitedRoom.roomId);
+            }
+
+            // also put this room in the virtual room ID cache so isVirtualRoom return the right answer
+            // in however long it takes for the echo of setAccountData to come down the sync
+            this.virtualRoomIdCache.add(invitedRoom.roomId);
+        }
+    }
+}
diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx
index 48d0eb2ab1..7a0ba58c97 100644
--- a/src/accessibility/KeyboardShortcuts.tsx
+++ b/src/accessibility/KeyboardShortcuts.tsx
@@ -168,6 +168,12 @@ const shortcuts: Record = {
                 key: Key.U,
             }],
             description: _td("Upload a file"),
+        }, {
+            keybinds: [{
+                modifiers: [CMD_OR_CTRL],
+                key: Key.F,
+            }],
+            description: _td("Search (must be enabled)"),
         },
     ],
 
diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx
index 0bb169abf8..9a7c1d1f0a 100644
--- a/src/accessibility/context_menu/MenuItem.tsx
+++ b/src/accessibility/context_menu/MenuItem.tsx
@@ -19,14 +19,23 @@ limitations under the License.
 import React from "react";
 
 import AccessibleButton from "../../components/views/elements/AccessibleButton";
+import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton";
 
 interface IProps extends React.ComponentProps {
     label?: string;
+    tooltip?: string;
 }
 
 // Semantic component for representing a role=menuitem
-export const MenuItem: React.FC = ({children, label, ...props}) => {
+export const MenuItem: React.FC = ({children, label, tooltip, ...props}) => {
     const ariaLabel = props["aria-label"] || label;
+
+    if (tooltip) {
+        return 
+            { children }
+        ;
+    }
+
     return (
         
             { children }
diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
index ab39a094db..863ee2b427 100644
--- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
+++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
@@ -95,7 +95,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
         const blob = new Blob([this._keyBackupInfo.recovery_key], {
             type: 'text/plain;charset=us-ascii',
         });
-        FileSaver.saveAs(blob, 'recovery-key.txt');
+        FileSaver.saveAs(blob, 'security-key.txt');
 
         this.setState({
             downloaded: true,
@@ -238,7 +238,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
             )}

{_t( "We'll store an encrypted copy of your keys on our server. " + - "Secure your backup with a recovery passphrase.", + "Secure your backup with a Security Phrase.", )}

{_t("For maximum security, this should be different from your account password.")}

@@ -252,10 +252,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent { onValidate={this._onPassPhraseValidate} fieldRef={this._passphraseField} autoFocus={true} - label={_td("Enter a recovery passphrase")} - labelEnterPassword={_td("Enter a recovery passphrase")} - labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")} - labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")} + label={_td("Enter a Security Phrase")} + labelEnterPassword={_td("Enter a Security Phrase")} + labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")} + labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")} /> @@ -270,7 +270,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{_t("Advanced")} - {_t("Set up with a recovery key")} + {_t("Set up with a Security Key")}
; @@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( - "Please enter your recovery passphrase a second time to confirm.", + "Please enter your Security Phrase a second time to confirm.", )}

@@ -319,7 +319,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { onChange={this._onPassPhraseConfirmChange} value={this.state.passPhraseConfirm} className="mx_CreateKeyBackupDialog_passPhraseInput" - placeholder={_t("Repeat your recovery passphrase...")} + placeholder={_t("Repeat your Security Phrase...")} autoFocus={true} />
@@ -338,15 +338,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent { _renderPhaseShowKey() { return

{_t( - "Your recovery key is a safety net - you can use it to restore " + - "access to your encrypted messages if you forget your recovery passphrase.", + "Your Security Key is a safety net - you can use it to restore " + + "access to your encrypted messages if you forget your Security Phrase.", )}

{_t( "Keep a copy of it somewhere secure, like a password manager or even a safe.", )}

- {_t("Your recovery key")} + {_t("Your Security Key")}
@@ -369,12 +369,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent { let introText; if (this.state.copied) { introText = _t( - "Your recovery key has been copied to your clipboard, paste it to:", + "Your Security Key has been copied to your clipboard, paste it to:", {}, {b: s => {s}}, ); } else if (this.state.downloaded) { introText = _t( - "Your recovery key is in your Downloads folder.", + "Your Security Key is in your Downloads folder.", {}, {b: s => {s}}, ); } @@ -433,14 +433,14 @@ export default class CreateKeyBackupDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { case PHASE_PASSPHRASE: - return _t('Secure your backup with a recovery passphrase'); + return _t('Secure your backup with a Security Phrase'); case PHASE_PASSPHRASE_CONFIRM: - return _t('Confirm your recovery passphrase'); + return _t('Confirm your Security Phrase'); case PHASE_OPTOUT_CONFIRM: return _t('Warning!'); case PHASE_SHOWKEY: case PHASE_KEEPITSAFE: - return _t('Make a copy of your recovery key'); + return _t('Make a copy of your Security Key'); case PHASE_BACKINGUP: return _t('Starting backup...'); case PHASE_DONE: diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index b1a14062f4..84cb58536a 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -235,7 +235,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const blob = new Blob([this._recoveryKey.encodedPrivateKey], { type: 'text/plain;charset=us-ascii', }); - FileSaver.saveAs(blob, 'recovery-key.txt'); + FileSaver.saveAs(blob, 'security-key.txt'); this.setState({ downloaded: true, @@ -593,10 +593,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { onValidate={this._onPassPhraseValidate} fieldRef={this._passphraseField} autoFocus={true} - label={_td("Enter a recovery passphrase")} - labelEnterPassword={_td("Enter a recovery passphrase")} - labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")} - labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")} + label={_td("Enter a Security Phrase")} + labelEnterPassword={_td("Enter a Security Phrase")} + labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")} + labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")} />
diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js index 4dd296a8f1..eeb68b94bd 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js @@ -19,7 +19,7 @@ import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../../languageHandler'; -import { MatrixClient } from 'matrix-js-sdk'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import * as sdk from '../../../../index'; diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js index e7bae3578b..670cb28b94 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js @@ -17,7 +17,7 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import { MatrixClient } from 'matrix-js-sdk'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption'; import * as sdk from '../../../../index'; import { _t } from '../../../../languageHandler'; diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js index 9f5045635d..8c09cc6d16 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.js @@ -58,7 +58,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { ; const newMethodDetected =

{_t( - "A new recovery passphrase and key for Secure Messages have been detected.", + "A new Security Phrase and key for Secure Messages have been detected.", )}

; const hackWarning =

{_t( diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js index cda353e717..b60e6fd3cb 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.js @@ -56,7 +56,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { >

{_t( - "This session has detected that your recovery passphrase and key " + + "This session has detected that your Security Phrase and key " + "for Secure Messages have been removed.", )}

{_t( diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index ebf5d536ec..b7a4e0960e 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -27,6 +27,7 @@ import {sortBy} from "lodash"; import {makeGroupPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; +import {mediaFromMxc} from "../customisations/Media"; const COMMUNITY_REGEX = /\B\+\S*/g; @@ -95,7 +96,7 @@ export default class CommunityProvider extends AutocompleteProvider { name={name || groupId} width={24} height={24} - url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} /> + url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} /> ), range, diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index a07ed29c7e..91fbea4d6a 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -23,7 +23,6 @@ interface IOptions { keys: Array; funcs?: Array<(T) => string>; shouldMatchWordsOnly?: boolean; - shouldMatchPrefix?: boolean; // whether to apply unhomoglyph and strip diacritics to fuzz up the search. Defaults to true fuzzy?: boolean; } @@ -56,12 +55,6 @@ export default class QueryMatcher { if (this._options.shouldMatchWordsOnly === undefined) { this._options.shouldMatchWordsOnly = true; } - - // By default, match anywhere in the string being searched. If enabled, only return - // matches that are prefixed with the query. - if (this._options.shouldMatchPrefix === undefined) { - this._options.shouldMatchPrefix = false; - } } setObjects(objects: T[]) { @@ -112,7 +105,7 @@ export default class QueryMatcher { resultKey = resultKey.replace(/[^\w]/g, ''); } const index = resultKey.indexOf(query); - if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { + if (index !== -1) { matches.push( ...candidates.map((candidate) => ({index, ...candidate})), ); diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 32eea55b0b..5f0cfc2df1 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -56,7 +56,6 @@ export default class UserProvider extends AutocompleteProvider { this.matcher = new QueryMatcher([], { keys: ['name'], funcs: [obj => obj.userId.slice(1)], // index by user id minus the leading '@' - shouldMatchPrefix: true, shouldMatchWordsOnly: false, }); @@ -155,6 +154,7 @@ export default class UserProvider extends AutocompleteProvider { const currentUserId = MatrixClientPeg.get().credentials.userId; this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId); + this.users = this.users.concat(this.room.getMembersWithMembership("invite")); this.users = sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 190b231b74..9d9d57d8a6 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -22,6 +22,7 @@ import classNames from "classnames"; import {Key} from "../../Keyboard"; import {Writeable} from "../../@types/common"; +import {replaceableComponent} from "../../utils/replaceableComponent"; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -76,6 +77,7 @@ export interface IProps extends IPosition { hasBackground?: boolean; // whether this context menu should be focus managed. If false it must handle itself managed?: boolean; + wrapperClassName?: string; // Function to be called on menu close onFinished(); @@ -90,6 +92,7 @@ interface IState { // Generic ContextMenu Portal wrapper // all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1} // this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines. +@replaceableComponent("structures.ContextMenu") export class ContextMenu extends React.PureComponent { private initialFocus: HTMLElement; @@ -299,7 +302,7 @@ export class ContextMenu extends React.PureComponent { // such that it does not leave the (padded) window. if (contextMenuRect) { const padding = 10; - adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height + padding); + adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding); } position.top = adjusted; @@ -365,7 +368,7 @@ export class ContextMenu extends React.PureComponent { return (

{ } // Placement method for to position context menu to right of elementRect with chevronOffset -export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => { +export const toRightOf = (elementRect: Pick, chevronOffset = 12) => { const left = elementRect.right + window.pageXOffset + 3; let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset; top -= chevronOffset + 8; // where 8 is half the height of the chevron return {left, top, chevronOffset}; }; -// Placement method for to position context menu right-aligned and flowing to the left of elementRect +// Placement method for 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) => { const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; @@ -416,6 +420,41 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None return menuOptions; }; +// Placement method for to position context menu right-aligned and flowing to the left of elementRect +// and always above elementRect +export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => { + const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; + + const buttonRight = elementRect.right + window.pageXOffset; + const buttonBottom = elementRect.bottom + window.pageYOffset; + const buttonTop = elementRect.top + window.pageYOffset; + // Align the right edge of the menu to the right edge of the button + menuOptions.right = window.innerWidth - buttonRight; + // Align the menu vertically on whichever side of the button has more space available. + if (buttonBottom < window.innerHeight / 2) { + menuOptions.top = buttonBottom + vPadding; + } else { + menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + } + + return menuOptions; +}; + +// Placement method for to position context menu right-aligned and flowing to the right of elementRect +// and always above elementRect +export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => { + const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; + + const buttonLeft = elementRect.left + window.pageXOffset; + const buttonTop = elementRect.top + window.pageYOffset; + // Align the left edge of the menu to the left edge of the button + menuOptions.left = buttonLeft; + // Align the menu vertically above the menu + menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding; + + return menuOptions; +}; + type ContextMenuTuple = [boolean, RefObject, () => void, () => void, (val: boolean) => void]; export const useContextMenu = (): ContextMenuTuple => { const button = useRef(null); @@ -430,6 +469,7 @@ export const useContextMenu = (): ContextMenuTuple< return [isOpen, button, open, close, setIsOpen]; }; +@replaceableComponent("structures.LegacyContextMenu") export default class LegacyContextMenu extends ContextMenu { render() { return this.renderMenu(false); diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js index a79bdafeb5..73359f17a5 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -21,7 +21,9 @@ import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; import classNames from 'classnames'; import * as FormattingUtils from '../../utils/FormattingUtils'; +import {replaceableComponent} from "../../utils/replaceableComponent"; +@replaceableComponent("structures.CustomRoomTagPanel") class CustomRoomTagPanel extends React.Component { constructor(props) { super(props); diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index cbfeff7582..c37ab3df48 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -16,8 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; import request from 'browser-request'; diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 4836b0f554..32db5c251c 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {Filter} from 'matrix-js-sdk'; +import {Filter} from 'matrix-js-sdk/src/filter'; import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import EventIndexPeg from "../../indexing/EventIndexPeg"; @@ -26,10 +26,12 @@ import { _t } from '../../languageHandler'; import BaseCard from "../views/right_panel/BaseCard"; import {RightPanelPhases} from "../../stores/RightPanelStorePhases"; import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice"; +import {replaceableComponent} from "../../utils/replaceableComponent"; /* * Component which shows the filtered file using a TimelinePanel */ +@replaceableComponent("structures.FilePanel") class FilePanel extends React.Component { static propTypes = { roomId: PropTypes.string.isRequired, @@ -45,7 +47,7 @@ class FilePanel extends React.Component { }; onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => { - if (room.roomId !== this.props.roomId) return; + if (room?.roomId !== this.props?.roomId) return; if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; if (ev.isBeingDecrypted()) { diff --git a/src/components/structures/GenericErrorPage.js b/src/components/structures/GenericErrorPage.js index ab7d4f9311..cfd2016d47 100644 --- a/src/components/structures/GenericErrorPage.js +++ b/src/components/structures/GenericErrorPage.js @@ -16,7 +16,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import {replaceableComponent} from "../../utils/replaceableComponent"; +@replaceableComponent("structures.GenericErrorPage") export default class GenericErrorPage extends React.PureComponent { static propTypes = { title: PropTypes.object.isRequired, // jsx for title diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js index 96aa1ba728..976b2d81a5 100644 --- a/src/components/structures/GroupFilterPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -30,7 +30,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; import SettingsStore from "../../settings/SettingsStore"; import UserTagTile from "../views/elements/UserTagTile"; +import {replaceableComponent} from "../../utils/replaceableComponent"; +@replaceableComponent("structures.GroupFilterPanel") class GroupFilterPanel extends React.Component { static contextType = MatrixClientContext; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index bbc4187298..b006b323fb 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -35,10 +35,12 @@ import GroupStore from '../../stores/GroupStore'; import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks"; -import {Group} from "matrix-js-sdk"; +import {Group} from "matrix-js-sdk/src/models/group"; import {allSettled, sleep} from "../../utils/promise"; import RightPanelStore from "../../stores/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; +import {mediaFromMxc} from "../../customisations/Media"; +import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( `

HTML for your community's page

@@ -367,8 +369,7 @@ class FeaturedUser extends React.Component { const permalink = makeUserPermalink(this.props.summaryInfo.user_id); const userNameNode = { name }; - const httpUrl = MatrixClientPeg.get() - .mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64); + const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64); const deleteButton = this.props.editing ?
; } - const httpInviterAvatar = this.state.inviterProfile ? - this._matrixClient.mxcUrlToHttp( - this.state.inviterProfile.avatarUrl, 36, 36, - ) : null; + const httpInviterAvatar = this.state.inviterProfile + ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36) + : null; const inviter = group.inviter || {}; let inviterName = inviter.userId; diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx new file mode 100644 index 0000000000..769775d549 --- /dev/null +++ b/src/components/structures/HostSignupAction.tsx @@ -0,0 +1,58 @@ +/* +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 from "react"; +import { + IconizedContextMenuOption, + IconizedContextMenuOptionList, +} from "../views/context_menus/IconizedContextMenu"; +import { _t } from "../../languageHandler"; +import { HostSignupStore } from "../../stores/HostSignupStore"; +import SdkConfig from "../../SdkConfig"; +import {replaceableComponent} from "../../utils/replaceableComponent"; + +interface IProps {} + +interface IState {} + +@replaceableComponent("structures.HostSignupAction") +export default class HostSignupAction extends React.PureComponent { + private openDialog = async () => { + await HostSignupStore.instance.setHostSignupActive(true); + } + + public render(): React.ReactNode { + const hostSignupConfig = SdkConfig.get().hostSignup; + if (!hostSignupConfig?.brand) { + return null; + } + + return ( + + + + ); + } +} diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index cd5510de9d..341ab2df71 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -17,7 +17,9 @@ limitations under the License. import React from "react"; import PropTypes from "prop-types"; import AutoHideScrollbar from "./AutoHideScrollbar"; +import {replaceableComponent} from "../../utils/replaceableComponent"; +@replaceableComponent("structures.IndicatorScrollbar") export default class IndicatorScrollbar extends React.Component { static propTypes = { // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index c8fcd7e9ca..d419c9de6e 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -15,16 +15,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {InteractiveAuth} from "matrix-js-sdk"; +import {InteractiveAuth} from "matrix-js-sdk/src/interactive-auth"; import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents'; import * as sdk from '../../index'; +import {replaceableComponent} from "../../utils/replaceableComponent"; export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); +@replaceableComponent("structures.InteractiveAuthComponent") export default class InteractiveAuthComponent extends React.Component { static propTypes = { // matrix client to use for UI auth requests @@ -177,7 +179,14 @@ export default class InteractiveAuthComponent extends React.Component { stageState: stageState, errorText: stageState.error, }, () => { - if (oldStage != stageType) this._setFocus(); + if (oldStage !== stageType) { + this._setFocus(); + } else if ( + !stageState.error && this._stageComponent.current && + this._stageComponent.current.attemptFailed + ) { + this._stageComponent.current.attemptFailed(); + } }); }; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 4445ff3ff8..cbfc7b476b 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -16,9 +16,11 @@ limitations under the License. import * as React from "react"; import { createRef } from "react"; +import classNames from "classnames"; +import { Room } from "matrix-js-sdk/src/models/room"; + import GroupFilterPanel from "./GroupFilterPanel"; import CustomRoomTagPanel from "./CustomRoomTagPanel"; -import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import RoomList from "../views/rooms/RoomList"; @@ -32,13 +34,15 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore"; -import {Key} from "../../Keyboard"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; +import {replaceableComponent} from "../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../customisations/Media"; +import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; +import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; interface IProps { isMinimized: boolean; @@ -48,6 +52,7 @@ interface IProps { interface IState { showBreadcrumbs: boolean; showGroupFilterPanel: boolean; + activeSpace?: Room; } // List of CSS classes which should be included in keyboard navigation within the room list @@ -59,6 +64,7 @@ const cssClasses = [ "mx_RoomSublist_showNButton", ]; +@replaceableComponent("structures.LeftPanel") export default class LeftPanel extends React.Component { private listContainerRef: React.RefObject = createRef(); private groupFilterPanelWatcherRef: string; @@ -72,11 +78,13 @@ export default class LeftPanel extends React.Component { this.state = { showBreadcrumbs: BreadcrumbsStore.instance.visible, showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), + activeSpace: SpaceStore.instance.activeSpace, }; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate); + SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); this.bgImageWatcherRef = SettingsStore.watchSetting( "RoomList.backgroundImage", null, this.onBackgroundImageUpdate); this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { @@ -94,9 +102,14 @@ export default class LeftPanel extends React.Component { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate); + SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); } + private updateActiveSpace = (activeSpace: Room) => { + this.setState({ activeSpace }); + }; + private onExplore = () => { dis.fire(Action.ViewRoomDirectory); }; @@ -118,7 +131,7 @@ export default class LeftPanel extends React.Component { let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage"); if (settingBgMxc) { - avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize); + avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize); } const avatarUrlProp = `url(${avatarUrl})`; @@ -284,17 +297,18 @@ export default class LeftPanel extends React.Component { private onKeyDown = (ev: React.KeyboardEvent) => { if (!this.focusedElement) return; - switch (ev.key) { - case Key.ARROW_UP: - case Key.ARROW_DOWN: + const action = getKeyBindingsManager().getRoomListAction(ev); + switch (action) { + case RoomListAction.NextRoom: + case RoomListAction.PrevRoom: ev.stopPropagation(); ev.preventDefault(); - this.onMoveFocus(ev.key === Key.ARROW_UP); + this.onMoveFocus(action === RoomListAction.PrevRoom); break; } }; - private onEnter = () => { + private selectRoom = () => { const firstRoom = this.listContainerRef.current.querySelector(".mx_RoomTile"); if (firstRoom) { firstRoom.click(); @@ -375,11 +389,13 @@ export default class LeftPanel extends React.Component { > @@ -388,12 +404,15 @@ export default class LeftPanel extends React.Component { } public render(): React.ReactNode { - const groupFilterPanel = !this.state.showGroupFilterPanel ? null : ( -
- - {SettingsStore.getValue("feature_custom_tags") ? : null} -
- ); + let leftLeftPanel; + if (this.state.showGroupFilterPanel) { + leftLeftPanel = ( +
+ + {SettingsStore.getValue("feature_custom_tags") ? : null} +
+ ); + } const roomList = { onBlur={this.onBlur} isMinimized={this.props.isMinimized} onResize={this.onResize} + activeSpace={this.state.activeSpace} />; const containerClasses = classNames({ "mx_LeftPanel": true, - "mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel, "mx_LeftPanel_minimized": this.props.isMinimized, }); @@ -417,7 +436,7 @@ export default class LeftPanel extends React.Component { return (
- {groupFilterPanel} + {leftLeftPanel}